feat(configurator): A5 - wizard schema (rename notes, add Express fields, retire manual currency_id)
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import tests
|
||||
|
||||
|
||||
def _backfill_currency(env):
|
||||
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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': [],
|
||||
}
|
||||
|
||||
|
||||
@@ -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 & 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 & Conditions (prints on customer docs)">
|
||||
<field name="terms_and_conditions" nolabel="1"
|
||||
placeholder="Customer-facing terms — seeded from company default."/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user