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 controllers
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizard
|
from . import wizard
|
||||||
|
from . import tests
|
||||||
|
|
||||||
|
|
||||||
def _backfill_currency(env):
|
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 + invoicing ----
|
||||||
currency_id = fields.Many2one(
|
# Express Orders (2026-05-26): currency is now driven by pricelist_id.
|
||||||
'res.currency', string='Currency',
|
# The legacy free-standing currency_id Many2one(res.currency) is retired;
|
||||||
default=lambda self: self.env.company.currency_id,
|
# 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(
|
invoice_strategy = fields.Selection(
|
||||||
[('deposit', 'Deposit'), ('progress', 'Progress Billing'),
|
[('deposit', 'Deposit'), ('progress', 'Progress Billing'),
|
||||||
('net_terms', 'Net Terms'), ('cod_prepay', 'COD / Prepay')],
|
('net_terms', 'Net Terms'), ('cod_prepay', 'COD / Prepay')],
|
||||||
@@ -164,7 +185,44 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ---- Notes ----
|
# ---- 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 ----
|
# ---- Lines ----
|
||||||
line_ids = fields.One2many(
|
line_ids = fields.One2many(
|
||||||
@@ -561,7 +619,12 @@ class FpDirectOrderWizard(models.Model):
|
|||||||
'x_fc_is_blanket_order': self.is_blanket_order,
|
'x_fc_is_blanket_order': self.is_blanket_order,
|
||||||
'x_fc_block_partial_shipments': self.block_partial_shipments,
|
'x_fc_block_partial_shipments': self.block_partial_shipments,
|
||||||
'origin': 'Direct Order',
|
'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': [],
|
'order_line': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -339,9 +339,17 @@
|
|||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
<page string="Notes" name="notes">
|
<page string="Notes & Terms" name="notes">
|
||||||
<field name="notes" nolabel="1"
|
<group>
|
||||||
placeholder="Internal notes for the estimator / planner - not shown to the customer."/>
|
<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>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user