feat(configurator): "PO Pending" escape hatch for customers who send PO later

Customer feedback: some customers don't send their PO with the
initial order — they send it days or weeks later. The system was
blocking SO confirmation without a PO, which forced the shop to
either wait on paperwork or ask a manager for a formal override.

New estimator-level path: a "PO Pending" boolean on sale.order +
an optional "PO Expected By" date.

  * Confirm without a PO# / PO document when PO Pending is ticked.
  * action_confirm skips the hard error if po_pending OR po_override
    is set (keeps the existing manager-override path too).
  * On confirm with PO Pending, the system schedules a chase
    activity for po_expected_date (or +3 days if blank), assigned
    via mail.activity so it shows up in the sales user's activity
    list. Chatter note logged so audit is obvious.
  * Direct-order wizard: po_number and po_attachment_file become
    optional. Ticking "PO Pending" in the wizard is the trade-in;
    a help note under the toggle explains the chase behaviour.
  * Once the PO arrives, user fills in the PO# / uploads the doc,
    and turns PO Pending off — existing downstream flow resumes.

Difference from x_fc_po_override (kept):
  * PO Override = manager waiver, permanent ("handshake deal").
  * PO Pending = estimator flag, time-boxed ("customer will send it
    by Friday").

fusion_plating_configurator → 19.0.14.0.0
fusion_plating_invoicing    → 19.0.3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-23 10:08:00 -04:00
parent 2d3ee03f86
commit 8142bd229a
7 changed files with 131 additions and 21 deletions

View File

@@ -58,10 +58,26 @@ class FpDirectOrderWizard(models.TransientModel):
help='Ship all-or-nothing; partial pickings are blocked.',
)
# ---- PO (required — that's what makes this a "direct" order) ----
po_number = fields.Char(string='Customer PO #', required=True)
po_attachment_file = fields.Binary(string='PO Document', required=True)
# ---- PO ----
# Originally required at wizard time — that's what makes this a
# "direct" order vs. a quote. Relaxed 2026-04-23: some customers
# don't send their PO until after the order is in progress. The
# wizard now accepts a PO Pending flag in lieu of a PO#/doc; the
# underlying SO is confirmed with a chase activity scheduled for
# the expected date.
po_number = fields.Char(string='Customer PO #')
po_attachment_file = fields.Binary(string='PO Document')
po_attachment_filename = fields.Char(string='PO Filename')
po_pending = fields.Boolean(
string='PO Pending',
help='Customer will send the PO later. Enter their expected '
'date below; a follow-up activity will remind sales to '
'chase the paperwork.',
)
po_expected_date = fields.Date(
string='PO Expected By',
help='When the customer promised to send the PO.',
)
# ---- Fulfilment (order-level) ----
delivery_method = fields.Selection(
@@ -193,15 +209,30 @@ class FpDirectOrderWizard(models.TransientModel):
self.ensure_one()
if not self.line_ids:
raise UserError(_('Add at least one part line before confirming.'))
if not self.po_attachment_file:
raise UserError(_('Upload the customer PO document.'))
# 1. Save the PO attachment once
po_att = self.env['ir.attachment'].create({
'name': self.po_attachment_filename or 'po.pdf',
'datas': self.po_attachment_file,
'mimetype': 'application/pdf',
})
# Accept EITHER a PO (document + number) OR the PO Pending
# flag. Customers who haven't sent paperwork yet use Pending;
# sales chases them on po_expected_date.
if not self.po_pending:
if not self.po_attachment_file:
raise UserError(_(
'Upload the customer PO document, or tick "PO Pending" '
'if the customer will send the PO later.'
))
if not self.po_number:
raise UserError(_(
'Enter the Customer PO #, or tick "PO Pending" if the '
'customer will send the PO later.'
))
# 1. Save the PO attachment if one was uploaded
po_att = False
if self.po_attachment_file:
po_att = self.env['ir.attachment'].create({
'name': self.po_attachment_filename or 'po.pdf',
'datas': self.po_attachment_file,
'mimetype': 'application/pdf',
})
# 2. Find or create the generic plating service product
product = self.env['product.product'].search(
@@ -226,9 +257,11 @@ class FpDirectOrderWizard(models.TransientModel):
'partner_shipping_id': (
self.partner_shipping_id.id or self.partner_id.id
),
'x_fc_po_number': self.po_number,
'x_fc_po_attachment_id': po_att.id,
'x_fc_po_received': True,
'x_fc_po_number': self.po_number or False,
'x_fc_po_attachment_id': po_att.id if po_att else False,
'x_fc_po_received': bool(po_att),
'x_fc_po_pending': self.po_pending,
'x_fc_po_expected_date': self.po_expected_date or False,
'x_fc_customer_job_number': self.customer_job_number or False,
'x_fc_planned_start_date': self.planned_start_date,
'x_fc_internal_deadline': self.internal_deadline,

View File

@@ -40,10 +40,22 @@
<field name="customer_job_number"/>
</group>
<group string="Purchase Order">
<field name="po_number"/>
<field name="po_number"
placeholder="Enter the customer PO number"/>
<field name="po_attachment_file"
filename="po_attachment_filename"/>
<field name="po_attachment_filename" invisible="1"/>
<field name="po_pending" widget="boolean_toggle"/>
<field name="po_expected_date"
invisible="not po_pending"/>
<div colspan="2" class="text-muted small"
invisible="not po_pending">
<i class="fa fa-info-circle me-1"/>
Order will confirm without a PO number or
document. A chase activity will be scheduled
for the expected date so sales follows up
with the customer.
</div>
</group>
</group>