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:
@@ -31,23 +31,36 @@ class SaleOrder(models.Model):
|
||||
"""Override to check account hold + customer PO# and trigger
|
||||
the invoice strategy."""
|
||||
for order in self:
|
||||
# --- Customer PO# required ---
|
||||
# --- Customer PO# required (with escape hatches) ---
|
||||
# Aerospace AP teams reject invoices without their PO#
|
||||
# quoted back. Catching this at SO confirm prevents the
|
||||
# whole downstream chain (CoC, BoL, invoice) from going
|
||||
# out unreferenced. The PO# is on `client_order_ref`
|
||||
# (Odoo standard) AND mirrored to `x_fc_po_number`
|
||||
# (FP-specific) — accept either as filled.
|
||||
#
|
||||
# Two escape hatches for real-world cases:
|
||||
# * x_fc_po_pending — estimator flag: "PO coming later".
|
||||
# Confirms the order without a PO number and schedules
|
||||
# a chase activity for x_fc_po_expected_date so sales
|
||||
# doesn't forget to chase the paperwork.
|
||||
# * x_fc_po_override — manager waiver: "proceed without
|
||||
# a formal PO (handshake deal)". Permanent.
|
||||
po_set = bool(order.client_order_ref) or bool(
|
||||
getattr(order, 'x_fc_po_number', False)
|
||||
)
|
||||
if not po_set:
|
||||
po_pending = bool(getattr(order, 'x_fc_po_pending', False))
|
||||
po_override = bool(getattr(order, 'x_fc_po_override', False))
|
||||
if not po_set and not po_pending and not po_override:
|
||||
raise UserError(_(
|
||||
'Cannot confirm SO "%(so)s" — Customer PO# is required.\n\n'
|
||||
'Set the customer\'s purchase order number in the '
|
||||
'"Customer Reference" field (or x_fc_po_number) before '
|
||||
'confirming. Aerospace customers\' AP teams reject '
|
||||
'invoices that don\'t quote their PO# back.'
|
||||
'confirming. If the customer will provide the PO '
|
||||
'later, tick "PO Pending" and set "PO Expected By" — '
|
||||
'the order will confirm with a follow-up activity to '
|
||||
'chase the paperwork. A Plating Manager can also set '
|
||||
'"PO Override" for handshake deals.'
|
||||
) % {'so': order.name})
|
||||
|
||||
# --- Account hold check ---
|
||||
@@ -73,6 +86,36 @@ class SaleOrder(models.Model):
|
||||
|
||||
res = super().action_confirm()
|
||||
|
||||
# --- PO-pending chase activity ---
|
||||
# Sales team needs to chase the customer for the real PO before
|
||||
# any invoice goes out. Schedule a follow-up on the expected
|
||||
# date (or 3 days out if unset).
|
||||
for order in self:
|
||||
if not getattr(order, 'x_fc_po_pending', False):
|
||||
continue
|
||||
if getattr(order, 'x_fc_po_number', False) or order.client_order_ref:
|
||||
continue # PO already set — nothing to chase
|
||||
from datetime import timedelta
|
||||
expected = (
|
||||
order.x_fc_po_expected_date
|
||||
or (fields.Date.context_today(order) + timedelta(days=3))
|
||||
)
|
||||
order.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
date_deadline=expected,
|
||||
summary=_('Chase customer PO for %s') % order.name,
|
||||
note=_(
|
||||
'Order confirmed with PO Pending. Follow up with '
|
||||
'%(partner)s for their PO number (and PDF if '
|
||||
'available), then tick PO Pending off and enter the '
|
||||
'PO# on the sale order.'
|
||||
) % {'partner': order.partner_id.display_name},
|
||||
)
|
||||
order.message_post(body=_(
|
||||
'Order confirmed without PO. Chase activity scheduled '
|
||||
'for %(date)s.'
|
||||
) % {'date': expected.strftime('%Y-%m-%d')})
|
||||
|
||||
# --- Invoice strategy automation (on confirm) ---
|
||||
for order in self:
|
||||
strategy = order.x_fc_invoice_strategy
|
||||
|
||||
Reference in New Issue
Block a user