changes
This commit is contained in:
@@ -56,7 +56,8 @@ class FpCertificate(models.Model):
|
||||
'merged = already in the issued CoC PDF',
|
||||
)
|
||||
|
||||
@api.depends('x_fc_job_id', 'state', 'message_ids', 'attachment_id')
|
||||
@api.depends('x_fc_job_id', 'state', 'message_ids', 'attachment_id',
|
||||
'x_fc_local_thickness_pdf')
|
||||
def _compute_fischer_visibility(self):
|
||||
QC = self.env.get('fusion.plating.quality.check')
|
||||
empty_qc = self.env['fusion.plating.quality.check'] if QC is not None else None
|
||||
@@ -65,7 +66,14 @@ class FpCertificate(models.Model):
|
||||
qc = empty_qc
|
||||
pdf = empty_att
|
||||
status = 'none'
|
||||
if QC is not None and rec.x_fc_job_id:
|
||||
# Cert-local upload wins over QC-side PDF (matches the
|
||||
# merge resolution order in fp_certificate.py).
|
||||
if rec.x_fc_local_thickness_pdf:
|
||||
if rec.state == 'issued' and rec.attachment_id:
|
||||
status = 'merged'
|
||||
else:
|
||||
status = 'pending'
|
||||
elif QC is not None and rec.x_fc_job_id:
|
||||
# Same lookup the merge method uses — passed-first,
|
||||
# then any QC with a PDF.
|
||||
qc = QC.sudo().search([
|
||||
|
||||
@@ -189,6 +189,15 @@ class FpJob(models.Model):
|
||||
back to partner-level send_coc / send_thickness_report flags.
|
||||
'none' returns empty (commercial customer, no paperwork).
|
||||
Unknown requirement codes default to {'coc'} as a safety net.
|
||||
|
||||
Bundling rule (2026-05-18 — Entech workflow): when a CoC is
|
||||
wanted AND thickness is wanted, the thickness data is delivered
|
||||
as page 2 of the CoC PDF (see _fp_merge_thickness_into_pdf),
|
||||
so we return ONE cert ({'coc'}) instead of two. A standalone
|
||||
thickness_report cert is only produced when thickness is wanted
|
||||
WITHOUT a CoC — a rare edge case kept for completeness.
|
||||
Action_issue's thickness-data gate enforces actual readings or
|
||||
a Fischerscope PDF on the merged CoC.
|
||||
"""
|
||||
self.ensure_one()
|
||||
req = (
|
||||
@@ -196,16 +205,17 @@ class FpJob(models.Model):
|
||||
and self.part_catalog_id.certificate_requirement
|
||||
) or 'inherit'
|
||||
if req == 'inherit':
|
||||
types = set()
|
||||
if self.partner_id.x_fc_send_coc:
|
||||
types.add('coc')
|
||||
if self.partner_id.x_fc_send_thickness_report:
|
||||
types.add('thickness_report')
|
||||
return types
|
||||
want_coc = bool(self.partner_id.x_fc_send_coc)
|
||||
want_thickness = bool(self.partner_id.x_fc_send_thickness_report)
|
||||
if want_coc:
|
||||
return {'coc'} # thickness gets merged in
|
||||
if want_thickness:
|
||||
return {'thickness_report'}
|
||||
return set()
|
||||
return {
|
||||
'none': set(),
|
||||
'coc': {'coc'},
|
||||
'coc_thickness': {'coc', 'thickness_report'},
|
||||
'coc_thickness': {'coc'}, # bundled — thickness on page 2
|
||||
}.get(req, {'coc'})
|
||||
|
||||
next_milestone_action = fields.Selection(
|
||||
@@ -308,9 +318,29 @@ class FpJob(models.Model):
|
||||
return fn()
|
||||
|
||||
def _action_open_draft_certs(self):
|
||||
"""Open the cert list filtered to draft certs for this job.
|
||||
Manager reviews each in turn and clicks Issue per-cert."""
|
||||
"""Open the Issue Certs wizard for this job's draft certs.
|
||||
|
||||
The wizard prompts for a Fischerscope upload + readings per cert
|
||||
that needs thickness data (bundled CoC or standalone thickness
|
||||
report). Pure CoC certs (no thickness needed) appear in the
|
||||
wizard too and just need a Confirm click. Cleaner than the old
|
||||
"list view → open each cert → click Issue" flow.
|
||||
|
||||
Falls back to the cert list view if the wizard model isn't
|
||||
installed (defensive — should always exist when this module is).
|
||||
"""
|
||||
self.ensure_one()
|
||||
Wizard = self.env.get('fp.cert.issue.wizard')
|
||||
if Wizard is not None:
|
||||
try:
|
||||
return Wizard.open_for_job(self)
|
||||
except UserError:
|
||||
raise
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"Job %s: cert issue wizard failed (%s) — "
|
||||
"falling back to cert list.", self.name, e,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Draft Certificates — %s') % self.name,
|
||||
@@ -1521,6 +1551,37 @@ class FpJob(models.Model):
|
||||
job.name, job.qty, job.qty_done or 0,
|
||||
job.qty_scrapped or 0, accounted, job.qty,
|
||||
))
|
||||
# Receiving reconciliation: parts must be physically
|
||||
# received before the job can close, and the count must
|
||||
# match what came out (done + scrapped + visual rejects).
|
||||
# Without this guard a job ships with the wrong cert qty,
|
||||
# or worse, with no closed receiving for the auditor to
|
||||
# trace back to. Same bypass flag covers both checks.
|
||||
if not job.qty_received:
|
||||
raise UserError(_(
|
||||
"Job %s cannot be marked Done — Quantity Received "
|
||||
"is blank. Close the receiving record for SO %s "
|
||||
"before completing this job."
|
||||
) % (
|
||||
job.name,
|
||||
job.sale_order_id.name if job.sale_order_id else '?',
|
||||
))
|
||||
rejects = job.qty_visual_inspection_rejects or 0
|
||||
accounted_out = (
|
||||
(job.qty_done or 0)
|
||||
+ (job.qty_scrapped or 0)
|
||||
+ rejects
|
||||
)
|
||||
if abs(job.qty_received - accounted_out) > 0.0001:
|
||||
raise UserError(_(
|
||||
"Job %s qty mismatch — received %g, but qty_done "
|
||||
"(%g) + qty_scrapped (%g) + visual rejects (%g) "
|
||||
"= %g. Reconcile before closing."
|
||||
) % (
|
||||
job.name, job.qty_received,
|
||||
job.qty_done or 0, job.qty_scrapped or 0,
|
||||
rejects, accounted_out,
|
||||
))
|
||||
# QC gate: customers flagged x_fc_requires_qc must have a
|
||||
# passed QC before the job closes. AS9100 / Nadcap compliance.
|
||||
if QC and not skip_qc_gate \
|
||||
@@ -1596,6 +1657,10 @@ class FpJob(models.Model):
|
||||
refund auto-link, and the legacy notification dispatch all
|
||||
look up by job_ref. Setting both ends keeps every consumer
|
||||
happy.
|
||||
|
||||
Phase A — mirrors x_fc_carrier_id and x_fc_outbound_shipment_id
|
||||
from the linked receiving so the delivery carries the shipping
|
||||
choices made at receipt time. Shipping crew can override later.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.delivery_id:
|
||||
@@ -1612,6 +1677,25 @@ class FpJob(models.Model):
|
||||
"Job %s: fusion.plating.delivery has no job link field; "
|
||||
"delivery created without job back-reference.", self.name,
|
||||
)
|
||||
# Mirror outbound carrier + shipment from the SO's first
|
||||
# receiving record. If there are multiple receivings (split
|
||||
# shipments), the shipping crew can change either field on the
|
||||
# delivery form. Defensive: skip when fields aren't present
|
||||
# (older instance) or no receiving exists.
|
||||
if (self.sale_order_id
|
||||
and 'x_fc_receiving_ids' in self.sale_order_id._fields
|
||||
and self.sale_order_id.x_fc_receiving_ids):
|
||||
recv = self.sale_order_id.x_fc_receiving_ids[:1]
|
||||
if 'x_fc_carrier_id' in Delivery._fields \
|
||||
and 'x_fc_carrier_id' in recv._fields \
|
||||
and recv.x_fc_carrier_id:
|
||||
vals['x_fc_carrier_id'] = recv.x_fc_carrier_id.id
|
||||
if 'x_fc_outbound_shipment_id' in Delivery._fields \
|
||||
and 'x_fc_outbound_shipment_id' in recv._fields \
|
||||
and recv.x_fc_outbound_shipment_id:
|
||||
vals['x_fc_outbound_shipment_id'] = (
|
||||
recv.x_fc_outbound_shipment_id.id
|
||||
)
|
||||
try:
|
||||
delivery = Delivery.create(vals)
|
||||
self.delivery_id = delivery.id
|
||||
@@ -1626,13 +1710,20 @@ class FpJob(models.Model):
|
||||
on a job that already has a CoC won't create another one.
|
||||
|
||||
Each cert is pre-populated with everything action_issue needs
|
||||
(partner, spec_reference, part_number, quantity_shipped, po,
|
||||
(partner, spec_reference, process_description, certified_by,
|
||||
contact_partner, part_number, quantity_shipped, NC qty, PO,
|
||||
SO link, job link) so the manager just reviews and clicks Issue.
|
||||
|
||||
Replaces the single-CoC implementation: now honours
|
||||
part.certificate_requirement (coc / coc_thickness / none /
|
||||
inherit) and partner-level send_coc / send_thickness_report
|
||||
flags. Closes spec gap C-G1.
|
||||
Resolution sources for the new prefill fields:
|
||||
- process_description ← recipe.name (the job's process root)
|
||||
- certified_by_id ← customer_spec.signer_user_id, falling
|
||||
back to company.x_fc_owner_user_id
|
||||
- contact_partner_id ← partner.x_fc_default_coc_contact_id
|
||||
- nc_quantity ← qty_scrapped + qty_visual_insp_rejects
|
||||
|
||||
Honours part.certificate_requirement (coc / coc_thickness /
|
||||
none / inherit) and partner-level send_coc /
|
||||
send_thickness_report flags. Closes spec gap C-G1.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if 'fp.certificate' not in self.env:
|
||||
@@ -1645,6 +1736,25 @@ class FpJob(models.Model):
|
||||
# Spec drives the cert spec_reference. The customer.spec was
|
||||
# auto-filled onto the job at confirm time (sale_order.py).
|
||||
spec = self.customer_spec_id
|
||||
# Recipe drives the process description on the cert. Was previously
|
||||
# sourced from sale_order.x_fc_coating_config_id (since retired);
|
||||
# recipe.name is the human-readable replacement.
|
||||
recipe = self.recipe_id
|
||||
# Signer resolution: per-spec override wins, company default fills.
|
||||
signer = False
|
||||
if spec and 'signer_user_id' in spec._fields:
|
||||
signer = spec.signer_user_id
|
||||
if not signer and 'x_fc_owner_user_id' in self.company_id._fields:
|
||||
signer = self.company_id.x_fc_owner_user_id
|
||||
# Contact: per-customer default; blank means manager picks at issue.
|
||||
contact = False
|
||||
if 'x_fc_default_coc_contact_id' in self.partner_id._fields:
|
||||
contact = self.partner_id.x_fc_default_coc_contact_id
|
||||
# NC qty: scrapped + visual rejects. Both NULL-safe.
|
||||
nc_qty = int(
|
||||
(self.qty_scrapped or 0)
|
||||
+ (self.qty_visual_inspection_rejects or 0)
|
||||
)
|
||||
for cert_type in sorted(required):
|
||||
# Idempotency per type.
|
||||
existing_dom = [('certificate_type', '=', cert_type)]
|
||||
@@ -1691,6 +1801,8 @@ class FpJob(models.Model):
|
||||
(self.qty_done or self.qty or 0)
|
||||
- (self.qty_scrapped or 0)
|
||||
)
|
||||
if 'nc_quantity' in Cert._fields:
|
||||
vals['nc_quantity'] = nc_qty
|
||||
if 'po_number' in Cert._fields and self.sale_order_id \
|
||||
and 'x_fc_po_number' in self.sale_order_id._fields:
|
||||
vals['po_number'] = (
|
||||
@@ -1703,8 +1815,12 @@ class FpJob(models.Model):
|
||||
vals['customer_job_no'] = (
|
||||
self.sale_order_id.x_fc_customer_job_number or ''
|
||||
)
|
||||
if 'process_description' in Cert._fields and coating:
|
||||
vals['process_description'] = coating.name or ''
|
||||
if 'process_description' in Cert._fields and recipe:
|
||||
vals['process_description'] = recipe.name or ''
|
||||
if 'certified_by_id' in Cert._fields and signer:
|
||||
vals['certified_by_id'] = signer.id
|
||||
if 'contact_partner_id' in Cert._fields and contact:
|
||||
vals['contact_partner_id'] = contact.id
|
||||
if 'entech_wo_number' in Cert._fields:
|
||||
vals['entech_wo_number'] = self.name or ''
|
||||
cert = Cert.create(vals)
|
||||
@@ -1728,6 +1844,107 @@ class FpJob(models.Model):
|
||||
) % {'t': cert_type, 'e': e})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Backfill — closed jobs missing certs, plus cleanup of legacy
|
||||
# duplicate thickness_report certs created before the bundling rule.
|
||||
# ------------------------------------------------------------------
|
||||
# One-shot management action for jobs that closed BEFORE the
|
||||
# _fp_create_certificates bug fix (e.g. WO-30040). Two passes:
|
||||
# 1. CREATE any missing draft cert per the (updated) resolver
|
||||
# 2. VOID legacy duplicate thickness_report certs that have a
|
||||
# paired CoC on the same job — the bundling rule says the
|
||||
# CoC carries the thickness data on page 2
|
||||
# Both passes are idempotent — safe to re-run.
|
||||
@api.model
|
||||
def action_backfill_missing_certs(self):
|
||||
Cert = self.env.get('fp.certificate')
|
||||
if Cert is None:
|
||||
raise UserError(_(
|
||||
'fp.certificate model is not installed. Install '
|
||||
'fusion_plating_certificates before running this action.'
|
||||
))
|
||||
candidate_jobs = self.search([('state', '=', 'done')])
|
||||
scanned = 0
|
||||
backfilled_jobs = self.env['fp.job']
|
||||
created_count = 0
|
||||
voided_count = 0
|
||||
has_job_link = 'x_fc_job_id' in Cert._fields
|
||||
for job in candidate_jobs:
|
||||
required = job._resolve_required_cert_types()
|
||||
if not required:
|
||||
continue
|
||||
scanned += 1
|
||||
existing_certs = (
|
||||
Cert.sudo().search([('x_fc_job_id', '=', job.id)])
|
||||
if has_job_link else
|
||||
(Cert.sudo().search([
|
||||
('sale_order_id', '=', job.sale_order_id.id),
|
||||
]) if job.sale_order_id else Cert.browse())
|
||||
)
|
||||
existing_types = set(existing_certs.mapped('certificate_type'))
|
||||
|
||||
# ---- Pass 1: create missing certs --------------------------
|
||||
missing = required - existing_types
|
||||
if missing:
|
||||
before = len(existing_certs)
|
||||
job._fp_create_certificates()
|
||||
# Re-read to get the freshly-created ones for pass 2.
|
||||
existing_certs = (
|
||||
Cert.sudo().search([('x_fc_job_id', '=', job.id)])
|
||||
if has_job_link else existing_certs
|
||||
)
|
||||
delta = max(len(existing_certs) - before, 0)
|
||||
if delta:
|
||||
backfilled_jobs |= job
|
||||
created_count += delta
|
||||
|
||||
# ---- Pass 2: void duplicate thickness_report certs ---------
|
||||
# Bundling rule (CLAUDE.md): when CoC + thickness are both
|
||||
# wanted, the CoC absorbs the thickness data. A leftover
|
||||
# draft thickness_report cert on the same job is now noise
|
||||
# and should not be issued. Void it with a clear reason so
|
||||
# the audit trail tells the story.
|
||||
if 'coc' in required and 'coc' in existing_types:
|
||||
dup_thickness = existing_certs.filtered(
|
||||
lambda c: (c.certificate_type == 'thickness_report'
|
||||
and c.state == 'draft')
|
||||
)
|
||||
for cert in dup_thickness:
|
||||
cert.sudo().write({
|
||||
'state': 'voided',
|
||||
'void_reason': (
|
||||
'Auto-voided: bundling rule — thickness '
|
||||
'data is delivered as page 2 of the paired '
|
||||
'CoC, not as a separate cert.'
|
||||
),
|
||||
})
|
||||
cert.message_post(body=_(
|
||||
'Auto-voided by cleanup: bundling rule routes '
|
||||
'thickness data to the CoC.'
|
||||
))
|
||||
voided_count += 1
|
||||
backfilled_jobs |= job
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Cert backfill + cleanup complete'),
|
||||
'message': _(
|
||||
'Scanned %(s)d closed jobs. Created %(c)d draft '
|
||||
'cert(s); voided %(v)d duplicate thickness_report '
|
||||
'cert(s) across %(j)d job(s).'
|
||||
) % {
|
||||
's': scanned,
|
||||
'c': created_count,
|
||||
'v': voided_count,
|
||||
'j': len(backfilled_jobs),
|
||||
},
|
||||
'sticky': True,
|
||||
'type': 'success' if (created_count or voided_count) else 'warning',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FpJobStep(models.Model):
|
||||
"""Phase 7 — adds the migration idempotency key on fp.job.step.
|
||||
|
||||
|
||||
@@ -823,16 +823,67 @@ class FpJobStep(models.Model):
|
||||
'state': state_label,
|
||||
})
|
||||
|
||||
def _fp_check_receiving_gate(self):
|
||||
"""Block step transitions until parts are physically received.
|
||||
|
||||
Applied to every step EXCEPT Contract Review (paperwork — doesn't
|
||||
need parts on the floor). Fires from both button_start and
|
||||
button_finish so an operator can't begin OR complete physical
|
||||
work before the receiving record is closed.
|
||||
|
||||
Manager bypass: ``fp_skip_receiving_gate=True`` in context. Same
|
||||
pattern as the qty / QC / bake gates. Audit trail is preserved
|
||||
via the state-transition tracking on chatter.
|
||||
|
||||
Threshold: SO ``x_fc_receiving_status == 'received'``. Post-Sub-8
|
||||
that's the terminal state (inspection moved into the recipe's
|
||||
racking step; ``'inspected'`` was dropped in the 2026-05-18
|
||||
cleanup).
|
||||
"""
|
||||
if self.env.context.get('fp_skip_receiving_gate'):
|
||||
return
|
||||
for step in self:
|
||||
if step._fp_is_contract_review_step():
|
||||
continue
|
||||
so = step.job_id.sale_order_id
|
||||
if not so:
|
||||
# Internal rework / no SO — gate doesn't apply.
|
||||
continue
|
||||
if 'x_fc_receiving_status' not in so._fields:
|
||||
# Defensive: configurator module not installed.
|
||||
continue
|
||||
if so.x_fc_receiving_status != 'received':
|
||||
label = dict(
|
||||
so._fields['x_fc_receiving_status'].selection
|
||||
).get(
|
||||
so.x_fc_receiving_status,
|
||||
so.x_fc_receiving_status or 'unknown',
|
||||
)
|
||||
raise UserError(_(
|
||||
'Step "%(step)s" cannot proceed — parts not received '
|
||||
'yet (SO %(so)s receiving status: %(status)s).\n\n'
|
||||
'Close the receiving record (Sales > %(so)s > '
|
||||
'Receiving) before starting or finishing work on '
|
||||
'this step. A manager can bypass this gate for '
|
||||
'documented exceptions.'
|
||||
) % {
|
||||
'step': step.name,
|
||||
'so': so.name or '?',
|
||||
'status': label,
|
||||
})
|
||||
|
||||
def button_start(self):
|
||||
"""Single source of truth for step start:
|
||||
1. Sub 13 predecessor gate (raise UserError if blocking)
|
||||
2. Policy B Contract Review auto-open (route to QA-005)
|
||||
3. Sub 8 Racking auto-open (route to racking inspection)
|
||||
4. super().button_start() + receiving soft check + serial
|
||||
promotion for the standard path
|
||||
2. Receiving gate (raise UserError if parts not received)
|
||||
3. Policy B Contract Review auto-open (route to QA-005)
|
||||
4. Sub 8 Racking auto-open (route to racking inspection)
|
||||
5. super().button_start() + serial promotion for the standard
|
||||
path
|
||||
|
||||
Manager bypasses available via context:
|
||||
fp_skip_predecessor_check=True skips the Sub 13 gate
|
||||
fp_skip_receiving_gate=True skips the receiving gate
|
||||
"""
|
||||
# ---- 1. Sub 13 predecessor gate ----------------------------------
|
||||
skip_pred = self.env.context.get('fp_skip_predecessor_check')
|
||||
@@ -863,7 +914,13 @@ class FpJobStep(models.Model):
|
||||
),
|
||||
))
|
||||
|
||||
# ---- 2. Policy B Contract Review auto-open -----------------------
|
||||
# ---- 2. Receiving gate -------------------------------------------
|
||||
# Hard block (replaces the prior soft chatter warning). The
|
||||
# helper exempts Contract Review steps internally, so contract
|
||||
# review can still auto-open below regardless of receiving state.
|
||||
self._fp_check_receiving_gate()
|
||||
|
||||
# ---- 3. Policy B Contract Review auto-open -----------------------
|
||||
for step in self:
|
||||
if step._fp_is_contract_review_step():
|
||||
action = step._fp_open_contract_review()
|
||||
@@ -873,7 +930,7 @@ class FpJobStep(models.Model):
|
||||
step._fp_promote_serials_on_start()
|
||||
return action
|
||||
|
||||
# ---- 3. Sub 8 Racking auto-open ----------------------------------
|
||||
# ---- 4. Sub 8 Racking auto-open ----------------------------------
|
||||
for step in self:
|
||||
if step._fp_is_racking_step():
|
||||
action = step._fp_open_racking_inspection()
|
||||
@@ -883,33 +940,18 @@ class FpJobStep(models.Model):
|
||||
step._fp_promote_serials_on_start()
|
||||
return action
|
||||
|
||||
# ---- 4. Standard path: start + receiving check + serial promote --
|
||||
# ---- 5. Standard path: start + serial promote --------------------
|
||||
result = super().button_start()
|
||||
for step in self:
|
||||
if step.state == 'in_progress':
|
||||
step._fp_promote_serials_on_start()
|
||||
so = step.job_id.sale_order_id
|
||||
if not so:
|
||||
continue
|
||||
recv = so.x_fc_receiving_status if (
|
||||
'x_fc_receiving_status' in so._fields
|
||||
) else None
|
||||
if recv in (False, None, 'not_received'):
|
||||
step.job_id.message_post(body=_(
|
||||
'Step "%(step)s" started before parts were received '
|
||||
'(SO %(so)s — receiving status: %(status)s). '
|
||||
'Confirm the parts are physically on the floor before '
|
||||
'continuing.'
|
||||
) % {
|
||||
'step': step.name,
|
||||
'so': so.name or '',
|
||||
'status': recv or 'unknown',
|
||||
})
|
||||
return result
|
||||
|
||||
def button_finish(self):
|
||||
# Policy B — block until QA-005 complete (when customer requires it).
|
||||
self._fp_check_contract_review_complete()
|
||||
# Receiving gate — same helper as button_start, exempts CR steps.
|
||||
self._fp_check_receiving_gate()
|
||||
# NOTE: racking inspection gate removed — racking is now a recipe
|
||||
# step, not a separate inspection workflow. _fp_check_racking_
|
||||
# inspection_complete() is kept as a helper for diagnostics but
|
||||
|
||||
@@ -175,10 +175,13 @@ class SaleOrder(models.Model):
|
||||
if recv_status == 'not_received':
|
||||
so.x_fc_workflow_stage = 'awaiting_parts'
|
||||
continue
|
||||
if recv_status in ('partial', 'received'):
|
||||
so.x_fc_workflow_stage = 'inspecting'
|
||||
if recv_status == 'partial':
|
||||
so.x_fc_workflow_stage = 'awaiting_parts'
|
||||
continue
|
||||
if recv_status == 'inspected':
|
||||
if recv_status == 'received':
|
||||
# Sub 8: 'received' is the terminal receiving state (no
|
||||
# more separate 'inspected'). Parts are on the floor;
|
||||
# inspection happens inside the recipe's racking step.
|
||||
if not so.x_fc_assigned_manager_id and not jobs:
|
||||
so.x_fc_workflow_stage = 'assign_work'
|
||||
continue
|
||||
@@ -562,16 +565,27 @@ class SaleOrder(models.Model):
|
||||
return True
|
||||
|
||||
def action_fp_accept_parts(self):
|
||||
"""Mark receiving accepted; flip SO receiving status to inspected."""
|
||||
"""Mark receiving complete; flip SO receiving status to received.
|
||||
|
||||
Sub 8 (2026-04-22) moved inspection out of receiving and into the
|
||||
recipe's racking step. Receiving's terminal state is now 'closed'
|
||||
(or legacy 'accepted'), which maps to SO status 'received'. The
|
||||
old 'inspected' SO status no longer exists.
|
||||
"""
|
||||
self.ensure_one()
|
||||
Recv = self.env.get('fp.receiving')
|
||||
if Recv is None:
|
||||
return False
|
||||
for rec in Recv.search([('sale_order_id', '=', self.id)]):
|
||||
if rec.state in ('draft', 'inspecting'):
|
||||
# Push receiving to its terminal state — 'closed' is the
|
||||
# post-Sub-8 terminal; 'accepted' kept as a legacy fallback
|
||||
# only for old records still in pre-Sub-8 states.
|
||||
if rec.state in ('draft', 'counted', 'staged'):
|
||||
rec.state = 'closed'
|
||||
elif rec.state in ('inspecting',):
|
||||
rec.state = 'accepted'
|
||||
if 'x_fc_receiving_status' in self._fields:
|
||||
self.x_fc_receiving_status = 'inspected'
|
||||
self.x_fc_receiving_status = 'received'
|
||||
self.message_post(body=_('Parts accepted — ready to assign manager.'))
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user