feat(configurator): A5 - wizard schema (rename notes, add Express fields, retire manual currency_id)

This commit is contained in:
gsinghpal
2026-05-26 20:58:23 -04:00
parent 08bc2b6a89
commit 92b690aef1
6 changed files with 204 additions and 8 deletions

View File

@@ -6,6 +6,7 @@
from . import controllers
from . import models
from . import wizard
from . import tests
def _backfill_currency(env):

View File

@@ -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
""")

View File

@@ -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

View File

@@ -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'))

View File

@@ -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': [],
}

View File

@@ -339,9 +339,17 @@
</group>
</group>
</page>
<page string="Notes" name="notes">
<field name="notes" nolabel="1"
placeholder="Internal notes for the estimator / planner - not shown to the customer."/>
<page string="Notes &amp; Terms" name="notes">
<group>
<group string="Order-Level Internal Notes (never prints)">
<field name="internal_notes" nolabel="1"
placeholder="Visible only to estimator / planner / shop."/>
</group>
<group string="Terms &amp; Conditions (prints on customer docs)">
<field name="terms_and_conditions" nolabel="1"
placeholder="Customer-facing terms — seeded from company default."/>
</group>
</group>
</page>
</notebook>