changes
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user