Files
Odoo-Modules/fusion_plating/fusion_plating_quality/scripts/sub12_e2e_walkthrough.py
gsinghpal f08f328688 changes
2026-04-27 00:11:18 -04:00

400 lines
17 KiB
Python

# -*- 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()