diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index 37c5f9e2..901dfd22 100644 --- a/fusion_plating/fusion_plating_configurator/__manifest__.py +++ b/fusion_plating/fusion_plating_configurator/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Configurator', - 'version': '19.0.13.6.0', + 'version': '19.0.14.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order.py b/fusion_plating/fusion_plating_configurator/models/sale_order.py index 012b9e18..c16b00b9 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order.py @@ -28,6 +28,25 @@ class SaleOrder(models.Model): x_fc_po_override = fields.Boolean(string='PO Override', help='Manager override — proceed without formal PO (handshake deal).') x_fc_po_override_reason = fields.Text(string='Override Reason') + # Estimator-level "PO is coming later" flag. Unlike PO Override + # (permanent, manager-only), this one is time-boxed: the order + # confirms with no PO yet, but a chase activity is scheduled for + # po_expected_date so sales chases the customer for the paperwork. + x_fc_po_pending = fields.Boolean( + string='PO Pending', + tracking=True, + help='Customer will provide the PO later. Confirms the order ' + 'without a PO number, schedules a chase activity, and ' + 'shows a "PO Pending" ribbon on the form. Toggle off once ' + 'the real PO arrives and you\'ve entered it below.', + ) + x_fc_po_expected_date = fields.Date( + string='PO Expected By', + tracking=True, + help='Date the customer promised to send the PO. A follow-up ' + 'activity is scheduled for this date when the order is ' + 'confirmed with PO Pending set.', + ) x_fc_invoice_strategy = fields.Selection( [('deposit', 'Deposit'), ('progress', 'Progress Billing'), ('net_terms', 'Net Terms'), ('cod_prepay', 'COD / Prepay')], diff --git a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml index f45f228b..289dc9cf 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -109,6 +109,9 @@ + + - + + + +
+ + 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. +
diff --git a/fusion_plating/fusion_plating_invoicing/__manifest__.py b/fusion_plating/fusion_plating_invoicing/__manifest__.py index dc4fb64d..6a1e73c2 100644 --- a/fusion_plating/fusion_plating_invoicing/__manifest__.py +++ b/fusion_plating/fusion_plating_invoicing/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Invoicing', - 'version': '19.0.2.3.0', + 'version': '19.0.3.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.', 'description': """ diff --git a/fusion_plating/fusion_plating_invoicing/models/sale_order.py b/fusion_plating/fusion_plating_invoicing/models/sale_order.py index 6f677af0..546ff245 100644 --- a/fusion_plating/fusion_plating_invoicing/models/sale_order.py +++ b/fusion_plating/fusion_plating_invoicing/models/sale_order.py @@ -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