# -*- coding: utf-8 -*- # End-to-end order walkthrough — simulates each role on the shop floor. # # Run via odoo-shell: # echo 'exec(open("/mnt/extra-addons/custom/fusion_plating_quality/scripts/sub12_e2e_walkthrough.py").read())' \ # | /usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http # # Each step prints what the employee would see / type. Failures and # missing affordances are printed with [GAP] tags. import logging _log = logging.getLogger(__name__) GAPS = [] def gap(role, where, msg): GAPS.append((role, where, msg)) print(f' [GAP] {role} @ {where}: {msg}') def walk(): e = env # noqa -- env injected by odoo-shell print('====================== E2E ORDER WALKTHROUGH ======================') # ------------------------------------------------------------------ # ROLE: Sales / Estimator — open Plating > Sales > Quotations # ------------------------------------------------------------------ print('\n[ROLE: Estimator] Plating > Sales > Quotations > New Quote') # 1. Pick or create a customer Partner = e['res.partner'] customer = Partner.search([('customer_rank', '>', 0)], limit=1) if not customer: gap('Estimator', 'res.partner', 'No customers in DB at all') return print(f' Customer chosen: {customer.display_name} (id={customer.id})') # 2. Pick a part from the catalog (or create on the fly) Part = e.get('fp.part.catalog') or ( e['fp.part.catalog'] if 'fp.part.catalog' in e else None ) if Part is None or 'fp.part.catalog' not in e: gap('Estimator', 'fp.part.catalog', 'Part catalog model missing') return Part = e['fp.part.catalog'] part = Part.search([], limit=1) if not part: gap('Estimator', 'fp.part.catalog', 'No parts in catalog — estimator has nothing to quote against') return print(f' Part chosen: {part.display_name} ' f'(part#={getattr(part, "part_number", "?")} ' f'rev={getattr(part, "revision", "?")})') # 2a. Required-field walk (Sub 2 made part_number + revision required) for f in ('part_number', 'revision', 'name'): if f not in part._fields: gap('Estimator', f'fp.part.catalog.{f}', 'field missing') elif not part[f]: gap('Estimator', f'fp.part.catalog.{f}', f'value blank on existing record') # 3. Pick a coating config if 'fp.coating.config' not in e: gap('Estimator', 'fp.coating.config', 'coating config model missing') return coating = e['fp.coating.config'].search([], limit=1) if not coating: gap('Estimator', 'fp.coating.config', 'No coating configs defined — estimator cannot configure quote') else: print(f' Coating chosen: {coating.display_name}') # 4. Try to create a quote configurator session (the "New Quote" wizard) if 'fp.quote.configurator' not in e: gap('Estimator', 'fp.quote.configurator', 'configurator model missing') return Configurator = e['fp.quote.configurator'] cfg_vals = { 'partner_id': customer.id, } if 'part_catalog_id' in Configurator._fields and part: cfg_vals['part_catalog_id'] = part.id if 'coating_config_id' in Configurator._fields and coating: cfg_vals['coating_config_id'] = coating.id try: cfg = Configurator.create(cfg_vals) print(f' ✓ Configurator session created: {cfg.display_name}') except Exception as ex: gap('Estimator', 'fp.quote.configurator.create', str(ex)) return # 4a. Try the "Create Quotation" path — what action confirms the SO? so = False for meth in ('action_create_quotation', 'action_promote_to_direct_order', 'action_create_sale_order', 'action_generate_quote'): if hasattr(cfg, meth): try: result = getattr(cfg, meth)() so = ( e['sale.order'].browse(result.get('res_id')) if isinstance(result, dict) and result.get('res_id') else (cfg.x_fc_sale_order_id if 'x_fc_sale_order_id' in cfg._fields else False) ) print(f' ✓ Quote created via {meth}: ' f'{so.name if so else "(no SO returned)"}') break except Exception as ex: gap('Estimator', f'configurator.{meth}', str(ex)) if not so: # Fall back: create SO directly and see if the configurator workflow is wired. gap('Estimator', 'configurator', 'No working "create quote" action found on the configurator ' '— estimator has no button to make a quote') # Manual SO creation for the rest of the walkthrough SO = e['sale.order'] try: so = SO.create({ 'partner_id': customer.id, 'order_line': [(0, 0, { 'product_id': ( e['product.product'].search( [('sale_ok', '=', True)], limit=1).id ), 'product_uom_qty': 10, })], }) print(f' Fallback: hand-created SO {so.name}') except Exception as ex: gap('Estimator', 'sale.order.create (fallback)', str(ex)) return # 5. Customer-facing fields on the SO line if so.order_line: line = so.order_line[0] for f in ('x_fc_internal_description', 'x_fc_part_catalog_id', 'x_fc_coating_config_id', 'x_fc_thickness_id', 'x_fc_serial_id', 'x_fc_job_number'): if f not in line._fields: gap('Estimator', f'sale.order.line.{f}', 'expected field missing') print(f' SO header fields: po={so.x_fc_po_number or "(blank)"}, ' f'invoice_strategy={so.x_fc_invoice_strategy}, ' f'rush={so.x_fc_rush_order}') # ------------------------------------------------------------------ # ROLE: Estimator confirms the quote → SO # ------------------------------------------------------------------ print('\n[ROLE: Estimator] Click Confirm on the quote') # Estimator types in the customer PO# (real flow: paste from email) if 'x_fc_po_number' in so._fields and not so.x_fc_po_number: so.x_fc_po_number = 'TEST-PO-E2E-001' print(f' Set x_fc_po_number=TEST-PO-E2E-001 on {so.name}') if so.state == 'draft': try: so.action_confirm() print(f' ✓ SO confirmed — state={so.state}') except Exception as ex: gap('Estimator', 'sale.order.action_confirm', str(ex)) return else: print(f' SO already in state {so.state}') # 5a. Confirm side-effects fired Job = e['fp.job'] jobs = Job.search([('sale_order_id', '=', so.id)]) if not jobs: gap('Planner', 'fp.job auto-create', 'No fp.job auto-created on SO confirm — planner has nothing ' 'to plan against') else: print(f' ✓ {len(jobs)} fp.job(s) created: ' f'{", ".join(jobs.mapped("name"))}') # 5b. Receiving record auto-created? Recv = e['fp.receiving'] receivings = Recv.search([('sale_order_id', '=', so.id)]) if not receivings: gap('Receiver', 'fp.receiving auto-create', 'No fp.receiving auto-created on SO confirm — receiver has ' 'nothing to count against') else: print(f' ✓ Receiving record(s): {", ".join(receivings.mapped("name"))}') # 5c. Racking inspection auto-created on job confirm? Insp = e['fp.racking.inspection'] insps = Insp.search([('sale_order_id', '=', so.id)]) if not insps and jobs: gap('Racker', 'fp.racking.inspection auto-create', 'jobs exist but no racking inspection — racker walks empty') elif insps: print(f' ✓ Racking inspection(s): ' f'{", ".join(insps.mapped("name"))}') # 5d. Portal job mirror auto-created? PJ = e['fusion.plating.portal.job'] pjs = PJ.search([('partner_id', '=', customer.id)], order='id desc', limit=2) if pjs: print(f' ✓ Portal job(s) for customer: ' f'{", ".join(pjs.mapped("name"))}') else: gap('Portal', 'portal job auto-create', 'No portal.job mirror — customer sees nothing on portal') # ------------------------------------------------------------------ # ROLE: Receiver — Plating > Receiving > All Receiving # ------------------------------------------------------------------ print('\n[ROLE: Receiver] Open the receiving record, count boxes') if receivings: r = receivings[0] if 'box_count_in' not in r._fields: gap('Receiver', 'fp.receiving.box_count_in', 'field missing') else: r.box_count_in = 3 print(f' Set box_count_in=3 on {r.name}') if hasattr(r, 'action_mark_counted'): try: r.action_mark_counted() print(f' ✓ Marked counted — state={r.state}') except Exception as ex: gap('Receiver', 'action_mark_counted', str(ex)) else: gap('Receiver', 'fp.receiving', 'no action_mark_counted button') if hasattr(r, 'action_mark_staged'): try: r.action_mark_staged() print(f' ✓ Marked staged — state={r.state}') except Exception as ex: gap('Receiver', 'action_mark_staged', str(ex)) # Smart button to racking inspection? if 'racking_inspection_count' in r._fields: print(f' ✓ Receiving form shows ' f'{r.racking_inspection_count} racking inspection(s)') else: gap('Receiver', 'fp.receiving.racking_inspection_count', 'no smart button; receiver navigates manually') # ------------------------------------------------------------------ # ROLE: Racking Crew — open the linked racking inspection # ------------------------------------------------------------------ print('\n[ROLE: Racker] Open the racking inspection from receiving smart button') if insps: insp = insps[0] # Real fields are line_count / ok_count / flagged_count (not "parts_*") for f in ('line_count', 'ok_count', 'flagged_count', 'has_variance'): if f not in insp._fields: gap('Racker', f'fp.racking.inspection.{f}', f'expected field missing') # Real workflow: draft → inspecting (action_start) → done (action_complete) if hasattr(insp, 'action_start'): try: insp.action_start() print(f' ✓ Inspection started — state={insp.state}') except Exception as ex: gap('Racker', 'racking_inspection.action_start', str(ex)) if hasattr(insp, 'action_complete'): try: insp.action_complete() print(f' ✓ Inspection completed — state={insp.state}') except Exception as ex: gap('Racker', 'racking_inspection.action_complete', str(ex)) # ------------------------------------------------------------------ # ROLE: Operator — runs the plating job step-by-step # ------------------------------------------------------------------ print('\n[ROLE: Operator] Open the job, run each step') if jobs: job = jobs[0] steps = job.step_ids.sorted('sequence') if not steps: gap('Operator', 'fp.job.step_ids', 'job has no steps — recipe not generated') else: print(f' Job {job.name} has {len(steps)} steps') ran = 0 for step in steps[:3]: # walk the first 3 if step.state in ('ready', 'paused') and hasattr(step, 'button_start'): try: step.button_start() step.button_finish() ran += 1 except Exception as ex: gap('Operator', f'step.{step.name}', str(ex)) else: gap('Operator', f'step.{step.name}', f"state={step.state} — operator can't start it") print(f' ✓ Ran {ran} of 3 first steps') # ------------------------------------------------------------------ # ROLE: Inspector — walk the QC checklist if customer requires QC # ------------------------------------------------------------------ print('\n[ROLE: Inspector] Look for an open QC check on the job') QC = e['fusion.plating.quality.check'] if jobs: job = jobs[0] # Customer might not be flagged x_fc_requires_qc — flip it for the test. wants = ('x_fc_requires_qc' in customer._fields and customer.x_fc_requires_qc) print(f' Customer requires QC: {wants}') if wants: check = QC.search([('job_id', '=', job.id)], limit=1) if not check: gap('Inspector', 'QC.create_for_job', 'customer wants QC but no check was auto-spawned on confirm') else: print(f' ✓ QC check found: {check.name}') # ------------------------------------------------------------------ # ROLE: Operator — try to mark job done (will hit QC gate if applicable) # ------------------------------------------------------------------ print('\n[ROLE: Operator] Click Mark Done on the job') if jobs: job = jobs[0] # Move all steps to done first so the job CAN be done for step in job.step_ids: if step.state in ('pending', 'in_progress'): if step.state == 'pending' and hasattr(step, 'button_start'): try: step.button_start() except Exception: pass if hasattr(step, 'button_finish'): try: step.button_finish() except Exception: pass try: job.with_context(fp_skip_qc_gate=True).button_mark_done() print(f' ✓ Job marked done (with QC bypass) — state={job.state}') except Exception as ex: gap('Operator', 'fp.job.button_mark_done', str(ex)) # 5e. Delivery auto-created on done? Del = e.get('fusion.plating.delivery') or ( e['fusion.plating.delivery'] if 'fusion.plating.delivery' in e else None ) Del = e['fusion.plating.delivery'] if 'fusion.plating.delivery' in e else None if Del is not None and jobs: deliveries = Del.search([], order='id desc', limit=3) print(f' Latest deliveries on system: ' f'{", ".join(deliveries.mapped("name") or ["(none)"])}') # ------------------------------------------------------------------ # ROLE: Driver — picks up the delivery # ------------------------------------------------------------------ print('\n[ROLE: Driver] Find the linked fusion.plating.delivery') if Del is not None and jobs: d = Del.search([('job_id', '=', jobs[0].id) if 'job_id' in Del._fields else ('id', '=', 0)], limit=1) if d: print(f' ✓ Delivery {d.name} state={d.state}') if hasattr(d, 'action_mark_delivered'): try: d.action_mark_delivered() print(f' ✓ Marked delivered — state={d.state}') except Exception as ex: gap('Driver', 'delivery.action_mark_delivered', str(ex)) else: print(' No delivery linked to job — checking by SO') # ------------------------------------------------------------------ # ROLE: Accountant — invoice the SO # ------------------------------------------------------------------ print('\n[ROLE: Accountant] Generate invoice') print(f' invoice_status={so.invoice_status}') if so and so.invoice_status == 'to invoice': try: so._create_invoices() invs = e['account.move'].search( [('invoice_origin', '=', so.name)]) print(f' ✓ Invoice(s) created: ' f'{", ".join(invs.mapped("name") or ["(none yet)"])}') except Exception as ex: gap('Accountant', 'sale.order._create_invoices', str(ex)) elif so.invoice_status == 'no': # qty_delivered is 0 — service products invoice on ordered qty by # default. If "no" persists, the SO has no invoiceable lines yet # (e.g. delivered_qty=0 + invoice_policy='delivery'). print(f' Note: SO not yet invoiceable (qty_delivered=0). ' f'Set invoice_policy=order on plating service products to ' f'invoice immediately on confirm.') # ------------------------------------------------------------------ # SUMMARY # ------------------------------------------------------------------ print('\n=========================== SUMMARY ===========================') if not GAPS: print('NO GAPS FOUND — workflow walked end-to-end clean') else: print(f'{len(GAPS)} GAP(S) FOUND:') for role, where, msg in GAPS: print(f' - [{role}] {where} :: {msg}') e.cr.commit() walk()