Digitize outsourcing (Odoo → outside digitizer)
You run the embroidery digitizing outsourcing loop. Embroidery artwork must
be digitized (turned into a stitch file) before it can be sewn; when that work
goes to an outside digitizer, the record that tracks it is an Odoo
digitize.demand. Your job, on a routine, is to send the Ready ones out and
mark them Outsourced.
This is the ERP half of the loop. The portal half — actually posting the
job into the digitizer's web form — is the separate
artworklady-digitizing skill, which you
invoke. Keep the two responsibilities separate: this skill decides what to
send and updates Odoo; that skill sends it.
SEND leg:
digitize_demand_search (Ready, non-custom) → pull decoration art + specs
→ download the PNG + AI files to disk
→ build the brief (H × W + thread codes + colours)
→ artworklady-digitizing submit (dry run, then confirm; standard turnaround)
→ on status=="submitted": digitize_demand_outsource (Ready → Outsource)
RETURN leg:
digitize_demand_search (state=outsource) → fetch the vendor's returned files
→ file them on your machine (persistent job archive + manifest)
→ digitize_demand_upload (Outsource → Sew Out)
→ parse_worksheet (the PDF) → stitch count, size, Stop-Sequence colours
→ digitize_demand_apply_worksheet (dims + stitch count + thread colours)
Transport
Odoo is driven through the drivethru_mcp MCP server via scripts/odoo_mcp.py:
python3 scripts/odoo_mcp.py tools # discover tools + schemas
python3 scripts/odoo_mcp.py call <tool> '{"...":"..."}' # call a tool
echo '{"...":"..."}' | python3 scripts/odoo_mcp.py call <tool>
Every call prints one JSON object, or {"error": {...}} with a non-zero exit.
ODOO_MCP_URL / ODOO_MCP_TOKEN must be set — if not, stop and tell the user.
Run tools first and trust its inputSchema over any signature below.
The routine, step by step
1. Read the Ready queue
python3 scripts/odoo_mcp.py call digitize_demand_search '{"is_custom": false}'
This returns Ready, non-custom demands — the queue (custom-text demands use a
different digitizing flow, so exclude them). Send only demands whose
decoration_method is exactly "Embroidery" — skip "Embroidery (Tackle Twill)",
Puff/3D embroidery, and any other variant. Also skip any demand whose decoration
already has a Shopworks ID (shopworks_design_number set): those are already
digitized in Shopworks and only need their file uploaded, not outsourcing — report
them so a human can upload, but never send them. Read the roll-up
(sendable_count, blocked_count, rush_count) and each row. Work at most ~20
per run, soonest earliest_ship_date first; anything beyond that is picked up
next run:
| Field | Use |
|---|---|
outsource_ready / blockers |
Only send rows where outsource_ready is true. Report the blocked ones (with their blockers), don't force them. |
decoration_method |
Send only when it is exactly "Embroidery" — skip Tackle Twill, Puff, and any other variant. |
shopworks_design_number |
If set, skip — already digitized in Shopworks; it only needs its file uploaded, not outsourcing. Report it for manual upload. |
decoration.id |
The job reference you send to the digitizer, and the record whose files you download. |
art.embroidery_png, art.ai_file |
The two files to download (cdn_url / web_url, present). |
art.height_in, art.width_in, art.color_names |
The brief — size and thread colours. |
earliest_ship_date, days_until_ship, is_rush |
Informational only — surfaced, not acted on (see §3). |
Skip anything already outsourced. The queue is Ready-only by default, so a
demand that has been sent is already gone from it; never widen with
include_all/is_outsourced and then send those.
2. Download the two files
For each sendable demand, get the decoration's embroidery production PNG and
its AI source file with digitize_demand_files — one call returns both:
python3 scripts/odoo_mcp.py call digitize_demand_files '{"demand_id": <DEMAND_ID>}'
Each entry comes back with filename, mimetype, size_bytes, source, and
data_base64 (or error). Decode each data_base64 to a file (e.g.
emb_<demand_id>.png, emb_<demand_id>.ai) and confirm it's non-empty.
Do not try to fetch the row's art.*.cdn_url yourself: on production those
binaries live on a CDN firewalled to Odoo's IP, so an agent can't reach it —
that's exactly why digitize_demand_files fetches them server-side and hands
you the bytes. If a file comes back with error (missing, or over the 20 MB
guard), that demand isn't sendable — report it and move on. See
references/routine.md for details.
3. Rush is informational — submit everything, standard turnaround
is_rush (ship date within the window) and days_until_ship are surfaced per
row, but the routine does not act on them right now. It submits every
outsource_ready, non-custom demand, and always at the standard nextday
turnaround — it does not select Artwork Lady's rush option. (The current
backlog trips is_rush on old ship dates that aren't true rushes, so a rush
turnaround would be wrong and can cost more.) Report the rush rows for
visibility, but don't treat them differently. Revisit if genuine rush handling
is wanted later.
4. Build the brief
The message you send the digitizer must clearly state the height × width and
the thread colours — each with its Odoo colour code. The code is essential: a
bare name like "White" lets the digitizer substitute a different thread code than
the one the shop picked (and the return leg then records their substitution over
your choice). Build the colour list from art.colors (each
{name, color_code, pms_color}), not color_names — format each as
<color_code> <name>:
Embroidery digitizing. Size: 3.5in W × 2.0in H.
Thread colours — use these exact codes: 1801 White, 1765 Tennessee Orange.
Left chest. Digitize to DST.
Build the size from art.width_in / art.height_in. If a colour has no
color_code on the row, use the name alone and flag it (the digitizer will pick
a code, which the return leg then records). Add placement/decoration context if
the row carries it.
5. Submit to the digitizer (dry run, then confirm)
Hand the actual submission to the artworklady-digitizing skill. Its creds
(ARTWORKLADY_USERNAME/PASSWORD, and, behind the egress proxy, the TLS SPKI
pins) are that skill's concern — if it exits config_error, stop and tell the
user to configure them; never ask for them in chat.
Always dry-run first (default confirm:false) to validate the fill, then
submit with confirm:true:
python3 <artworklady-digitizing>/scripts/artworklady_digitizing.py submit '{
"job_reference": "<DECO_ID>",
"turnaround": "nextday",
"file_types": ["dst"],
"files": ["/abs/emb_<id>.png", "/abs/emb_<id>.ai"],
"message": "Embroidery digitizing. Size: 3.5in W × 2.0in H. Thread colours — use these exact codes: 1801 White, 1765 Tennessee Orange. …"
}'
job_reference= the decoration id (what that skill expects).files— the PNG first (the mandatory upload slot), the AI second. Both go up.turnaround— alwaysnextdayfor now; do not selectrushorder/sameday(see §3). Revisit only if genuine rush handling is turned on.- Success is judged on the real submit response (HTTP 200 →
status:"submitted"). Treat onlystatus:"submitted"as sent — never adry_run, anerror, or the sight of/thanks.
6. Mark it Outsourced — exactly once
Only on status:"submitted", transition the demand:
python3 scripts/odoo_mcp.py call digitize_demand_outsource \
'{"demand_id": <DEMAND_ID>, "note": "Artwork Lady — job_reference <DECO_ID>, submitted <ISO time>"}'
This moves the demand ready → outsource, stamps outsource_date, and flags
is_outsourced on the demand and the decoration. Do this immediately after
each confirmed submission, one demand at a time.
digitize_demand_outsource is idempotent (a demand already outsourced comes
back as already_outsourced), but the window between a confirmed send and this
call is the one risk: the portal returns no id, so a resend can't be detected.
If this call fails after a confirmed submit, stop and report it — do not
re-submit the job.
Idempotency & exactly-once (read this)
- Work only
state=readydemands. Once sent they're atoutsourceand drop out of the default queue, so a re-run never re-sends them. - Submit first, mark outsourced after a
submittedresult — and put the outsource call right after the submit for each demand, so a crash leaves the demand Ready (correct: it was never actually sent) rather than sent-but-unmarked. - The portal does not dedupe and returns no id. The demand's state + the
chatter
note(job reference + submitted-at) are your audit trail.
Return leg: receive the digitized file (Outsource → Sew Out)
When the digitizer finishes, pull the stitch file back and complete the demand.
- Find the demands waiting on the digitizer:
python3 scripts/odoo_mcp.py call digitize_demand_search '{"state":["outsource"]}'
Process only demands whose decoration_method is exactly "Embroidery" (skip
Tackle Twill / Puff / other variants). Work at most ~20 per run; the rest come
next hour.
- Fetch the vendor's returned files from the portal with the
artworklady-digitizingskill'sfetchcommand. It logs into our account, finds our order row on/ordersby job reference (the decoration id), and downloads that row's own download links — so we get the files the vendor actually produced. Fetch straight into the job's archive folder so the files land where you keep them (see step 3):
python3 <artworklady-digitizing>/scripts/artworklady_digitizing.py fetch \
'{"job_reference": "<DECO_ID>", "out_dir": "/home/agent/.openclaw/jobs/<DECO_ID>"}'
status:"fetched"→ take the stitch file from thedstpath in the result, and the worksheet PDF from the.pdfentry infiles(there's always one for the job) — you need it in steps 5–6.status:"pending"→ the order isn't finished yet; skip and retry next pass.status:"not_found"→ no order for that job reference; report it.
Do not guess the file URL. The vendor's file store is shared across
customers, so a job-number collision can rename the file (e.g. 8524_2.DST);
scraping our own order row is what guarantees the right file.
- File the vendor's return package on your own machine. Keep everything the
order returned (typically the worksheet PDF, the DST, the native
.EMBdesign, a.TBF) in a per-job folder on persistent storage, and drop amanifest.jsonbeside it so the folder is self-describing:
# the archive folder IS the fetch out_dir above — persistent, survives redeploys
JOB=/home/agent/.openclaw/jobs/<DECO_ID>
cat > "$JOB/manifest.json" <<JSON
{"decoration_id": <DECO_ID>, "job_reference": "<DECO_ID>",
"customer": "<customer>", "job_name": "<decoration name>",
"digitize_demand_id": <DEMAND_ID>, "artwork_lady_order_id": "<order # if known>",
"date_filed_utc": "<UTC now>", "files": [{"filename": "...", "bytes": 0}]}
JSON
- Persistent, not
/tmp. The archive must live on durable storage — for this agent that's the mounted volume under/home/agent/.openclaw/…; anything under/tmpis wiped on redeploy. If your persistent workspace is elsewhere, use that (and use.../vendor-files/<DECO_ID>/ifjobs/is taken). - This is deliberately not stored in Odoo — it's the shop's own copy of what the vendor sent, like a job folder on an employee's work machine. When someone asks for a job's files later, look here by decoration id.
- Re-filing a job just overwrites its folder, so a re-run is safe.
- Upload the DST (and the worksheet PDF) back to Odoo, which writes them onto
the decoration and advances the demand
outsource → sew_out:
DST_B64=$(base64 -w0 "$JOB"/*.DST)
PDF_B64=$(base64 -w0 "$JOB"/*.pdf)
python3 scripts/odoo_mcp.py call digitize_demand_upload \
"{\"demand_id\": <DEMAND_ID>, \"dst_base64\": \"$DST_B64\", \"pdf_base64\": \"$PDF_B64\"}"
(Read them from the archive folder you filed in step 3, and pipe the JSON via
stdin — the base64 is large.) digitize_demand_upload is idempotent — a
demand already at Sew Out/Done comes back as already_uploaded, so a re-run
skips finished work. Leave stitch_count to step 6, which is the single
source of truth for the worksheet numbers.
- Parse the worksheet PDF.
scripts/parse_worksheet.pyreads the stitch count, sewn size, and the Stop-Sequence thread list positionally (seereferences/routine.md):
python3 scripts/parse_worksheet.py "$JOB"/*.pdf --pretty
# → {"stitch_count":5455,"height_in":2.08,"width_in":4.01,
# "colors":[{"sequence":1,"code":"1765","name":"Tiger",…},
# {"sequence":2,"code":"1801","name":"Super White",…}], "warnings":[]}
Read warnings — a colour count mismatch or a millimetre size means eyeball
the PDF before applying. A row with no code can't be matched; surface it.
- Apply the worksheet to the decoration. Pass the parsed values straight
through —
colorsneeds onlycode,name,sequenceper row:
python3 scripts/odoo_mcp.py call digitize_demand_apply_worksheet '{
"demand_id": <DEMAND_ID>,
"stitch_count": 5455, "height_in": 2.08, "width_in": 4.01,
"colors": [{"sequence":1,"code":"1765","name":"Tiger"},
{"sequence":2,"code":"1801","name":"Super White"}]
}'
This writes number_of_stitches, size_height/size_width, and rebuilds the
decoration's thread colours in stitch order — matching each by its code. A
code not on file is created (name + code) and a note is posted on the
demand's batch @mentioning the thread buyer (default Laura McCarter) to buy
the thread or change the colour. The result's colors.matched /
colors.created and note tell you what happened; report any created
colours so a human follows up. Re-running is safe — a created colour matches
next time, so the note fires once. Dry-run first with "dry_run": true if you
want to preview the matches and creates without writing.
A pass, end to end
Send leg:
digitize_demand_search '{"is_custom": false}'→ the Ready, non-custom queue.- If
sendable_countis 0, reportblocked_count+ top blockers and stop. - Keep only demands whose
decoration_methodis exactly "Embroidery" (skip Tackle Twill / Puff / other variants) and with noshopworks_design_number(already digitized in Shopworks — report for manual upload, don't send), then take up to ~20outsource_ready, soonestearliest_ship_datefirst. - For each, one at a time: download PNG + AI → build the brief →
artworklady-digitizing submitdry run → confirm (standardnextdayturnaround) → onsubmitted,digitize_demand_outsource. - Report: how many outsourced, which were blocked (and why), and any rush rows (informational only — still sent at standard turnaround).
Return leg:
digitize_demand_search '{"state":["outsource"]}'→ demands out at the digitizer. Process only Embroidery-method demands (skip Tackle Twill / Puff); cap ~20/run.- For each,
artworklady-digitizing fetchinto the job's archive folder (out_dir=/home/agent/.openclaw/jobs/<deco id>); skip any that come backpending(not finished yet). - File the vendor return package: the fetch already put the files in the archive
folder — write a
manifest.jsonbeside them. Persistent storage, not/tmp; not Odoo. - base64 the archived
.DST(+ the.pdfaspdf_base64) anddigitize_demand_uploadit → the demand moves to Sew Out. parse_worksheet.py <archived .pdf>→digitize_demand_apply_worksheetwith the stitch count, size, and Stop-Sequence colours. Report how many were received vs still pending, any thread colours that had to be created (batch note raised), and where each job was filed.
Errors
config_error(exit 2) —ODOO_MCP_URL/ODOO_MCP_TOKENmissing (this skill), orARTWORKLADY_USERNAME/PASSWORDmissing (the digitizing skill).connection_error— Odoo unreachable or key rejected.- A tool that ran but failed returns a normal MCP result with
isError: trueand a human-readable message — surface it. portal_errorfrom the digitizing skill — the submit did not return HTTP 200; the demand stays Ready and is retried next pass. Do NOT mark it outsourced.
References
references/routine.md— the download details, the turnaround/rush mapping, file naming, and the exactly-once reasoning in full.
