#!/usr/bin/env bash # ===================================================================== # import-merchant-catalog.sh — onboard a merchant on a LogShip instance and import a product # catalog (JSON) WITH images, prices and opening stock, all through the app's own routes. # # ADMIN_USER=demo ADMIN_PASS=… MERCHANT_PASS=… \ # MERCHANT_NAME="Kazaar Fragrances" MERCHANT_SHORT=kazaar CATALOG=/root/demo-setup/kazaar/catalog.json \ # ./import-merchant-catalog.sh # # Catalog entry (see demo-instance/kazaar/build-catalog.py): # { value, name, grossPrice, netPrice, upc, sku, weight, length, width, height, group1, group2, # classification, description, documentNote, imageUrl, stock } # value = search key (SKU), group1 = brand (later the iDempiere brand entity), group2 = reference / # "inspired by", classification = product kind. imageUrl is downloaded (cached in IMG_DIR) and sent # to /api/materials/products/quick-store as the product image (Strapi). # # Env (defaults in brackets): BASE [http://127.0.0.1:3001], ADMIN_USER/ADMIN_PASS, MERCHANT_PASS, # MERCHANT_NAME, MERCHANT_SHORT, MERCHANT_CEO [""], MERCHANT_EMAIL [logistik@.example], # MERCHANT_STREET/ZIP/CITY, MERCHANT_TAXID, BINS [40], BIN_PREFIX [KA], CLIENT_ID [1000000], # ROLE_ID [1000000], ORG_ID [1000000], WH_ID [1000000], PRICE_LIST_ID [1000003], PLV_NET [1000002], # PLV_GROSS [1000003], DOCTYPE_INV [1000023], SHIPPER_ID [1000000], LOCATOR_TYPE_SHELF [1000002], # BP_GROUP_MERCHANT [1000001], COUNTRY_ID [101], IMG_DIR [/root/demo-setup/images/], # IMG_DELAY [0.5] seconds between image downloads, LIMIT [0 = all entries]. # # Idempotent: merchant, bins and every article (by value) are looked up first; opening stock is one # completed Physical Inventory per run for the articles that have no stock yet. Re-run after a # failure to continue. Needs curl + jq + python3. # ===================================================================== set -euo pipefail BASE="${BASE:-http://127.0.0.1:3001}" ADMIN_USER="${ADMIN_USER:?}"; ADMIN_PASS="${ADMIN_PASS:?}"; MERCHANT_PASS="${MERCHANT_PASS:?}" MERCHANT_NAME="${MERCHANT_NAME:?}"; MERCHANT_SHORT="${MERCHANT_SHORT:?}"; CATALOG="${CATALOG:?}" MERCHANT_CEO="${MERCHANT_CEO:-}"; MERCHANT_EMAIL="${MERCHANT_EMAIL:-logistik@${MERCHANT_SHORT}.example}" MERCHANT_STREET="${MERCHANT_STREET:-Musterstraße 1}"; MERCHANT_ZIP="${MERCHANT_ZIP:-10115}"; MERCHANT_CITY="${MERCHANT_CITY:-Berlin}" MERCHANT_TAXID="${MERCHANT_TAXID:-}" BINS="${BINS:-40}"; BIN_PREFIX="${BIN_PREFIX:-KA}" CLIENT_ID="${CLIENT_ID:-1000000}"; ROLE_ID="${ROLE_ID:-1000000}"; ORG_ID="${ORG_ID:-1000000}"; WH_ID="${WH_ID:-1000000}" PRICE_LIST_ID="${PRICE_LIST_ID:-1000003}"; PLV_NET="${PLV_NET:-1000002}"; PLV_GROSS="${PLV_GROSS:-1000003}" DOCTYPE_INV="${DOCTYPE_INV:-1000023}"; SHIPPER_ID="${SHIPPER_ID:-1000000}"; LOCATOR_TYPE_SHELF="${LOCATOR_TYPE_SHELF:-1000002}" BP_GROUP_MERCHANT="${BP_GROUP_MERCHANT:-1000001}"; COUNTRY_ID="${COUNTRY_ID:-101}" IMG_DIR="${IMG_DIR:-/root/demo-setup/images/$MERCHANT_SHORT}"; IMG_DELAY="${IMG_DELAY:-0.5}"; LIMIT="${LIMIT:-0}" TODAY="$(date +%Y-%m-%d)" JAR="$(mktemp)"; trap 'rm -f "$JAR"' EXIT mkdir -p "$IMG_DIR" say() { printf '\n== %s\n' "$*"; } api() { if [ $# -ge 3 ]; then curl -sS -b "$JAR" -c "$JAR" -X "$1" "$BASE$2" -H 'Content-Type: application/json' -d "$3"; else curl -sS -b "$JAR" -c "$JAR" -X "$1" "$BASE$2"; fi; } odata() { local enc; enc="$(jq -rn --arg s "\$filter=$2&\$top=${3:-50}" '$s|@uri')"; api GET "/api/filters/$1/$enc"; } first_id() { jq -r '(.records // .)[0].id // empty'; } ok() { local st; st="$(echo "$1" | jq -r '.status // 200' 2>/dev/null || echo 500)"; if [ "$st" != 200 ]; then echo " FAILED: $(echo "$1" | cut -c1-300)" >&2; return 1; fi; } esc() { printf "%s" "$1" | sed "s/'/''/g"; } # --- 0. sign in -------------------------------------------------------------------------------- say "sign in as $ADMIN_USER" login="$(api POST /api/idempiere-auth/login "$(jq -n --arg u "$ADMIN_USER" --arg p "$ADMIN_PASS" '{userName:$u,password:$p,selectRole:true}')")" echo "$login" | jq -e '.token' >/dev/null || { echo "login failed" >&2; exit 1; } api POST /api/idempiere-auth/select-role "$(jq -n --argjson c "$CLIENT_ID" --argjson r "$ROLE_ID" --argjson o "$ORG_ID" --argjson w "$WH_ID" '{clientId:$c,roleId:$r,organizationId:$o,warehouseId:$w,language:"de_DE"}')" >/dev/null api GET /api/idempiere-auth/authenticated | jq -e '.user.id' >/dev/null || { echo "session not authenticated" >&2; exit 1; } echo " ok" # --- 1. merchant ---------------------------------------------------------------------------------- say "merchant '$MERCHANT_NAME' (org $MERCHANT_SHORT)" MBP="$(odata c_bpartner "Name eq '$(esc "$MERCHANT_NAME")'" | first_id)" if [ -n "$MBP" ]; then MORG="$(odata ad_org "Value eq '$MERCHANT_SHORT'" | first_id)"; MWH="$(odata m_warehouse "AD_Org_ID eq $MORG" | first_id)" echo " exists: bpartner $MBP org $MORG warehouse $MWH" else res="$(api POST /api/admin/merchant-onboarding/process "$(jq -n --arg n "$MERCHANT_NAME" --arg ceo "$MERCHANT_CEO" --arg s "$MERCHANT_SHORT" --arg p "$MERCHANT_PASS" \ --arg e "$MERCHANT_EMAIL" --arg st "$MERCHANT_STREET" --arg z "$MERCHANT_ZIP" --arg c "$MERCHANT_CITY" --arg tax "$MERCHANT_TAXID" \ --argjson g "$BP_GROUP_MERCHANT" --argjson pl "$PRICE_LIST_ID" --argjson sh "$SHIPPER_ID" --argjson co "$COUNTRY_ID" '{ companyName:$n, ceoName:$ceo, shortName:$s, isFulfillmentCustomer:true, taxId:$tax, companyEmail:$e, street:$st, zip:$z, city:$c, countryId:$co, partnerGroupId:$g, priceListId:$pl, shipperOrderDefaultId:$sh, deliveryViaRuleId:"D", password:$p, fulfillmentOrderBaseprice:1.99, fulfillmentOrderPickprice:0.35, fulfillmentOrderQtyPickfree:1, fulfillmentOrderReturnBaseprice:2.49, fulfillmentOrderReturnPickprice:0.35, shelfRentLargePrice:5.5, shelfRentSmallPrice:3.9, rentPalettSpacePrice:12.9, fulfillmentServiceHourlyRate:39, paketSPrice:0.45, paketMPrice:0.65, paketLPrice:0.95, paketXLPrice:1.45, logshipMonthlyFee:99 }')")" echo "$res" | jq -r '.report[] | " step \(.step) \(.entity): \(.status) \(.id // "") \(.message // "")"' ok "$res" || exit 1 MBP="$(echo "$res" | jq -r .ctx.bpartnerId)"; MORG="$(echo "$res" | jq -r .ctx.newOrgId)"; MWH="$(echo "$res" | jq -r .ctx.warehouseId)" echo " created: bpartner $MBP org $MORG warehouse $MWH" fi [ -n "$MORG" ] && [ -n "$MWH" ] || { echo "merchant org/warehouse unresolved" >&2; exit 1; } # --- 2. bins --------------------------------------------------------------------------------------- say "$BINS bins in warehouse $MWH" LOC_IDS=() for n in $(seq 1 "$BINS"); do aisle=$(printf '%02d' $(( (n-1)/5 + 1 ))); pos=$(printf '%02d' $(( (n-1)%5 + 1 ))); b="$BIN_PREFIX-$aisle-$pos" id="$(odata m_locator "M_Warehouse_ID eq $MWH and Value eq '$b'" | first_id)" if [ -z "$id" ]; then res="$(api POST /api/materials/locators/store "$(jq -n --argjson o "$MORG" --argjson w "$MWH" --arg v "$b" --arg x "$BIN_PREFIX" --arg y "$aisle" --arg z "$pos" --argjson t "$LOCATOR_TYPE_SHELF" \ '{organizationId:$o, warehouseId:$w, value:$v, priorityNo:50, isActive:true, isDefault:false, x:$x, y:$y, z:$z, locatorTypeId:$t}')")" ok "$res" || exit 1; id="$(echo "$res" | jq -r .id)" fi LOC_IDS+=("$id") done echo " ${#LOC_IDS[@]} bins ready" # --- 3. articles ------------------------------------------------------------------------------------ say "articles from $CATALOG (org $MORG)" TOTAL="$(jq length "$CATALOG")"; [ "$LIMIT" -gt 0 ] && TOTAL=$(( LIMIT < TOTAL ? LIMIT : TOTAL )) created=0; existed=0; failed=0; NEW_IDS=(); NEW_STOCK=() for i in $(seq 0 $((TOTAL-1))); do e="$(jq -c ".[$i]" "$CATALOG")" value="$(echo "$e" | jq -r .value)"; name="$(echo "$e" | jq -r .name)" id="$(odata m_product "AD_Org_ID eq $MORG and Value eq '$(esc "$value")'" | first_id)" if [ -n "$id" ]; then existed=$((existed+1)); continue; fi # image (cached per URL hash) imgurl="$(echo "$e" | jq -r '.imageUrl // empty')"; imgarg=() if [ -n "$imgurl" ]; then f="$IMG_DIR/$(printf '%s' "$imgurl" | md5sum | cut -c1-16).img" if [ ! -s "$f" ]; then curl -sSL -A "Mozilla/5.0" --max-time 30 -o "$f" "$imgurl" || true; sleep "$IMG_DELAY" fi if [ -s "$f" ]; then mime="$(file -b --mime-type "$f" 2>/dev/null || echo image/png)" case "$mime" in image/*) imgarg=(-F "image=@$f;type=$mime;filename=$value.${mime#image/}");; *) rm -f "$f";; esac fi fi gross="$(echo "$e" | jq -r .grossPrice)"; net="$(echo "$e" | jq -r .netPrice)" res="$(curl -sS -b "$JAR" -c "$JAR" -X POST "$BASE/api/materials/products/quick-store" \ -F "name=$name" -F "price=$gross" -F "organizationId=$MORG" \ -F "priceListVersionIds=[$PLV_NET,$PLV_GROSS]" -F "priceListVersionPrices={\"$PLV_NET\":$net,\"$PLV_GROSS\":$gross}" "${imgarg[@]}")" if ! ok "$res" 2>/dev/null; then failed=$((failed+1)); echo " FAILED $value: $(echo "$res" | cut -c1-200)"; continue; fi id="$(echo "$res" | jq -r '.id // .product.id')" # master data (fail-soft, but reported) upd="$(api PUT /api/materials/products/update "$(echo "$e" | jq -c --argjson id "$id" --argjson o "$MORG" \ '{id:$id, organizationId:$o, value:.value, sku:(.sku // ""), upc:(.upc // ""), weight:.weight, length:.length, width:.width, height:.height, group1:(.group1 // ""), group2:(.group2 // ""), classification:(.classification // ""), description:(.description // ""), documentNote:(.documentNote // "")}')")" ok "$upd" 2>/dev/null || echo " (details not saved for $value: $(echo "$upd" | cut -c1-120))" created=$((created+1)); NEW_IDS+=("$id"); NEW_STOCK+=("$(echo "$e" | jq -r '.stock // 25')") printf ' %4d/%d %s (%s) img=%s prices=%s\n' "$((i+1))" "$TOTAL" "$value" "$id" "$(echo "$res" | jq -r .imageUploaded)" "$(echo "$res" | jq -c '[.prices[]?.ok]')" done echo " created $created, existed $existed, failed $failed" # --- 4. opening stock for the new articles (chunks of 100 lines) ---------------------------------------- if [ "${#NEW_IDS[@]}" -gt 0 ]; then say "opening stock for ${#NEW_IDS[@]} new articles" chunk=0 for start in $(seq 0 100 $(( ${#NEW_IDS[@]} - 1 ))); do chunk=$((chunk+1)) lines="$(for n in $(seq "$start" $(( start+99 ))); do [ "$n" -lt "${#NEW_IDS[@]}" ] || break; jq -n --argjson p "${NEW_IDS[$n]}" --argjson l "${LOC_IDS[$(( n % ${#LOC_IDS[@]} ))]}" --argjson q "${NEW_STOCK[$n]}" --argjson ln "$(( (n-start+1)*10 ))" \ '{M_Product_ID:{id:$p}, M_Locator_ID:{id:$l}, qtyCount:$q, qtyBook:0, line:$ln}'; done | jq -s .)" res="$(api POST /api/materials/stock-takes/store "$(jq -n --argjson o "$MORG" --argjson w "$MWH" --argjson d "$DOCTYPE_INV" --arg dt "$TODAY" --arg desc "Erstbestand $MERCHANT_NAME ($TODAY) #$chunk" --argjson lines "$lines" \ '{organizationId:$o, warehouseId:$w, docTypeId:$d, movementDate:$dt, isActive:true, description:$desc, inventoryLines:$lines}')")" ok "$res" || exit 1; inv="$(echo "$res" | jq -r .id)" res="$(api POST "/api/materials/stock-takes/$inv/document-action" '{"documentAction":"CO"}')"; ok "$res" || exit 1 echo " inventory $inv: $(echo "$res" | jq -r '.DocStatus.identifier // .DocStatus.id // "?"')" done fi say "done — merchant org $MORG, warehouse $MWH, articles created $created / existing $existed / failed $failed"