From 92b690aef1dc54315cf97a3d5ef35480df910d71 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 26 May 2026 20:58:23 -0400 Subject: [PATCH] feat(configurator): A5 - wizard schema (rename notes, add Express fields, retire manual currency_id) --- .../fusion_plating_configurator/__init__.py | 1 + .../migrations/19.0.22.0.0/pre-migrate.py | 84 +++++++++++++++++++ .../tests/__init__.py | 9 ++ .../tests/test_express_wizard_fields.py | 31 +++++++ .../wizard/fp_direct_order_wizard.py | 73 ++++++++++++++-- .../wizard/fp_direct_order_wizard_views.xml | 14 +++- 6 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 fusion_plating/fusion_plating_configurator/migrations/19.0.22.0.0/pre-migrate.py create mode 100644 fusion_plating/fusion_plating_configurator/tests/__init__.py create mode 100644 fusion_plating/fusion_plating_configurator/tests/test_express_wizard_fields.py diff --git a/fusion_plating/fusion_plating_configurator/__init__.py b/fusion_plating/fusion_plating_configurator/__init__.py index 39cece35..f461c5ff 100644 --- a/fusion_plating/fusion_plating_configurator/__init__.py +++ b/fusion_plating/fusion_plating_configurator/__init__.py @@ -6,6 +6,7 @@ from . import controllers from . import models from . import wizard +from . import tests def _backfill_currency(env): diff --git a/fusion_plating/fusion_plating_configurator/migrations/19.0.22.0.0/pre-migrate.py b/fusion_plating/fusion_plating_configurator/migrations/19.0.22.0.0/pre-migrate.py new file mode 100644 index 00000000..a8952f8f --- /dev/null +++ b/fusion_plating/fusion_plating_configurator/migrations/19.0.22.0.0/pre-migrate.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""Pre-migration for Express Orders (19.0.22.0.0). + +Runs BEFORE Odoo's field-registration pass on `-u fusion_plating_configurator`, +so the model can register `terms_and_conditions` and `pricelist_id` against +columns that already hold data. + +Actions: + 1. Rename `fp_direct_order_wizard.notes` → `terms_and_conditions`. + Existing data preserves its semantic (always was customer-facing because + the old `action_create_order` wrote it to sale.order.note). + 2. Add the new Express columns (idempotent — IF NOT EXISTS guards). + 3. Backfill `pricelist_id` from the legacy `currency_id` via any active + pricelist matching the currency. After this, the model's stored-related + currency_id (related='pricelist_id.currency_id') takes over. + 4. (No currency_id column drop here — Odoo's schema sync recognises the + related field and keeps the column shape; data refreshes from the + related lookup on subsequent writes.) + +Dev-stage caveat: per the Express Orders design spec section 12, we are +ignoring past orders. If `currency_id` data on legacy rows doesn't line up +with the new pricelist_id-driven flow, that's acceptable. +""" + + +def migrate(cr, version): + # 1. Rename notes → terms_and_conditions (same column, same data) + cr.execute(""" + SELECT 1 FROM information_schema.columns + WHERE table_name = 'fp_direct_order_wizard' + AND column_name = 'notes' + """) + if cr.fetchone(): + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + RENAME COLUMN notes TO terms_and_conditions + """) + + # 2. Add new Express columns (idempotent) + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + ADD COLUMN IF NOT EXISTS internal_notes TEXT + """) + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + ADD COLUMN IF NOT EXISTS pricelist_id INTEGER + REFERENCES product_pricelist(id) ON DELETE SET NULL + """) + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + ADD COLUMN IF NOT EXISTS material_process VARCHAR + """) + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + ADD COLUMN IF NOT EXISTS validity_date DATE + """) + cr.execute(""" + ALTER TABLE fp_direct_order_wizard + ADD COLUMN IF NOT EXISTS view_source VARCHAR DEFAULT 'legacy' + """) + # Note: view_source defaults to 'legacy' for EXISTING rows — they were + # created via the legacy view. New rows default to 'express' via the model. + + # 3. Backfill pricelist_id from any active pricelist matching the + # legacy currency_id (only if the currency_id column still exists). + cr.execute(""" + SELECT 1 FROM information_schema.columns + WHERE table_name = 'fp_direct_order_wizard' + AND column_name = 'currency_id' + """) + if cr.fetchone(): + cr.execute(""" + UPDATE fp_direct_order_wizard w + SET pricelist_id = ( + SELECT p.id + FROM product_pricelist p + WHERE p.currency_id = w.currency_id + AND p.active = TRUE + ORDER BY p.id + LIMIT 1 + ) + WHERE w.pricelist_id IS NULL + AND w.currency_id IS NOT NULL + """) diff --git a/fusion_plating/fusion_plating_configurator/tests/__init__.py b/fusion_plating/fusion_plating_configurator/tests/__init__.py new file mode 100644 index 00000000..937ad87d --- /dev/null +++ b/fusion_plating/fusion_plating_configurator/tests/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# Part of the Fusion Plating product family. + +from . import test_express_part_defaults +from . import test_express_line_fields +from . import test_express_so_line_fields +from . import test_express_sale_order_fields +from . import test_express_wizard_fields diff --git a/fusion_plating/fusion_plating_configurator/tests/test_express_wizard_fields.py b/fusion_plating/fusion_plating_configurator/tests/test_express_wizard_fields.py new file mode 100644 index 00000000..517639dc --- /dev/null +++ b/fusion_plating/fusion_plating_configurator/tests/test_express_wizard_fields.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Express Orders — Task A5 schema tests +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install', 'fp_express') +class TestExpressWizardFields(TransactionCase): + def test_new_fields_exist(self): + Wiz = self.env['fp.direct.order.wizard'] + for fname in ( + 'material_process', 'pricelist_id', 'validity_date', + 'internal_notes', 'terms_and_conditions', 'view_source', + ): + self.assertIn(fname, Wiz._fields, msg=f'Missing field: {fname}') + + def test_old_notes_retired(self): + Wiz = self.env['fp.direct.order.wizard'] + # `notes` was renamed to terms_and_conditions in the pre-migration. + self.assertNotIn('notes', Wiz._fields) + + def test_view_source_default_express(self): + partner = self.env['res.partner'].create({'name': 'C'}) + wiz = self.env['fp.direct.order.wizard'].create({'partner_id': partner.id}) + self.assertEqual(wiz.view_source, 'express') + + def test_currency_id_is_related_from_pricelist(self): + Wiz = self.env['fp.direct.order.wizard'] + currency_field = Wiz._fields.get('currency_id') + self.assertIsNotNone(currency_field) + # Stored related from pricelist_id.currency_id + self.assertEqual(currency_field.related, ('pricelist_id', 'currency_id')) diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py index 443739ca..4cd32733 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py @@ -139,10 +139,31 @@ class FpDirectOrderWizard(models.Model): ) # ---- Currency + invoicing ---- - currency_id = fields.Many2one( - 'res.currency', string='Currency', - default=lambda self: self.env.company.currency_id, + # Express Orders (2026-05-26): currency is now driven by pricelist_id. + # The legacy free-standing currency_id Many2one(res.currency) is retired; + # currency_id stays as a stored-related so existing Monetary widgets keep + # working without manifest churn. + pricelist_id = fields.Many2one( + 'product.pricelist', + string='Pricelist', + default=lambda self: self._fp_default_pricelist(), + help='Drives both the order currency and the price computation. ' + 'Replaces the legacy currency_id Many2one(res.currency). ' + 'Customer-default flows in via res.partner.property_product_pricelist ' + 'on partner pick.', ) + currency_id = fields.Many2one( + 'res.currency', + related='pricelist_id.currency_id', + store=True, + string='Currency', + readonly=True, + ) + + @api.model + def _fp_default_pricelist(self): + """Default pricelist = company's default. Re-resolved on partner pick.""" + return self.env.company.partner_id.property_product_pricelist.id or False invoice_strategy = fields.Selection( [('deposit', 'Deposit'), ('progress', 'Progress Billing'), ('net_terms', 'Net Terms'), ('cod_prepay', 'COD / Prepay')], @@ -164,7 +185,44 @@ class FpDirectOrderWizard(models.Model): ) # ---- Notes ---- - notes = fields.Text(string='Internal Notes') + # Express Orders (2026-05-26): the legacy `notes` field was misleadingly + # labelled "Internal Notes" but its action_create_order path actually + # writes it to sale.order.note (which IS customer-facing and prints on + # quote / SO / invoice PDFs). Split into two distinct fields: + # - terms_and_conditions → sale.order.note (customer-facing, prints) + # - internal_notes → sale.order.x_fc_internal_notes (internal-only) + # Pre-migration renames the existing `notes` column → `terms_and_conditions` + # so the existing data preserves its customer-facing semantic. + terms_and_conditions = fields.Text( + string='Terms & Conditions', + help='Customer-facing terms — prints on quote / SO / invoice. ' + 'Seeded from res.company.invoice_terms_html with partner-level ' + 'override via res.partner.invoice_terms.', + ) + internal_notes = fields.Text( + string='Order-Level Internal Notes', + help='Visible only to the estimator / planner / shop. Never prints.', + ) + + # ---- Express Orders header (2026-05-26) ---- + material_process = fields.Char( + string='Material / Process Tag', + help='Free-text shop tag (e.g. ENP-STEEL-HP-ADVANCED). Informational.', + ) + validity_date = fields.Date( + string='Quote Validity', + help='Mirrors sale.order.validity_date — when the quote/SO expires.', + ) + view_source = fields.Selection( + [('express', 'Express Orders View'), + ('legacy', 'Legacy Direct Order View')], + string='View Source', + default='express', + required=True, + readonly=True, + help='Which view created this draft. Drafts list routes click-action ' + 'to the matching form. Dropped at phase-out Phase 4.', + ) # ---- Lines ---- line_ids = fields.One2many( @@ -561,7 +619,12 @@ class FpDirectOrderWizard(models.Model): 'x_fc_is_blanket_order': self.is_blanket_order, 'x_fc_block_partial_shipments': self.block_partial_shipments, 'origin': 'Direct Order', - 'note': self.notes or False, + 'note': self.terms_and_conditions or False, + # Express Orders header (2026-05-26) + 'x_fc_internal_notes': self.internal_notes or False, + 'x_fc_material_process': self.material_process or False, + 'pricelist_id': self.pricelist_id.id if self.pricelist_id else False, + 'validity_date': self.validity_date or False, 'order_line': [], } diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml index d86eb943..042bbef7 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml @@ -339,9 +339,17 @@ - - + + + + + + + + +