changes
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Invoicing',
|
||||
'version': '19.0.3.2.0',
|
||||
'version': '19.0.3.3.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
|
||||
'description': """
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Backfill plating defaults from fp.invoice.strategy.default → res.partner.
|
||||
|
||||
v3.3 merges the per-customer invoice strategy onto the partner record
|
||||
itself so the new "Plating Defaults" tab is the single source of truth.
|
||||
Only plain columns are migrated here (invoice_strategy + deposit %).
|
||||
The legacy `fp.invoice.strategy.default` model is left in place; the
|
||||
new sale.order onchange falls back to it for any partner whose record
|
||||
hasn't been migrated, so downstream code keeps working mid-rollout.
|
||||
|
||||
property_payment_term_id is intentionally skipped — it lives in
|
||||
ir_property rather than as a plain column, and the legacy onchange
|
||||
fallback already reads payment_term from the strategy default record
|
||||
when the partner doesn't have one set directly.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return
|
||||
cr.execute("""
|
||||
UPDATE res_partner p
|
||||
SET x_fc_default_invoice_strategy = COALESCE(
|
||||
p.x_fc_default_invoice_strategy, isd.default_strategy),
|
||||
x_fc_default_deposit_percent = COALESCE(
|
||||
NULLIF(p.x_fc_default_deposit_percent, 0),
|
||||
isd.default_deposit_percent)
|
||||
FROM fp_invoice_strategy_default isd
|
||||
WHERE isd.partner_id = p.id
|
||||
""")
|
||||
_logger.info(
|
||||
'fusion_plating_invoicing migration 19.0.3.3.0: backfilled %d '
|
||||
'partner records from fp.invoice.strategy.default',
|
||||
cr.rowcount,
|
||||
)
|
||||
@@ -9,6 +9,7 @@ from odoo import fields, models
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# ===== Account hold (existing) ============================================
|
||||
x_fc_account_hold = fields.Boolean(
|
||||
string='Account Hold', tracking=True,
|
||||
help='When active, blocks SO confirmation, invoicing, and shipping.',
|
||||
@@ -20,3 +21,45 @@ class ResPartner(models.Model):
|
||||
x_fc_account_hold_by_id = fields.Many2one(
|
||||
'res.users', string='Hold Placed By',
|
||||
)
|
||||
|
||||
# ===== Plating Defaults (cascade onto every new SO for this customer) =====
|
||||
# The estimator sets these once on the customer record; they pre-fill
|
||||
# invoice strategy, delivery method, and deadlines on every new SO so
|
||||
# repeat customers don't need re-typing the same values each order.
|
||||
# Tax type lives on `property_account_position_id` (Odoo native fiscal
|
||||
# position) and payment terms on `property_payment_term_id` — both are
|
||||
# surfaced on the same Plating Defaults tab in the partner form.
|
||||
|
||||
x_fc_default_invoice_strategy = fields.Selection(
|
||||
[('deposit', 'Deposit'),
|
||||
('progress', 'Progress Billing'),
|
||||
('net_terms', 'Net Terms'),
|
||||
('cod_prepay', 'COD / Prepay')],
|
||||
string='Default Invoice Strategy',
|
||||
help='Pre-fills the SO invoice strategy when this customer is selected. '
|
||||
'The estimator can still override per order.',
|
||||
)
|
||||
x_fc_default_deposit_percent = fields.Float(
|
||||
string='Default Deposit %',
|
||||
help='Used when invoice strategy is "Deposit". e.g. 50.0 for 50%.',
|
||||
)
|
||||
x_fc_default_delivery_method = fields.Selection(
|
||||
[('local_delivery', 'Local Delivery'),
|
||||
('shipping_partner', 'Shipping Partner'),
|
||||
('customer_pickup', 'Customer Pickup')],
|
||||
string='Default Delivery Method',
|
||||
help='Pre-fills the SO delivery method when this customer is selected.',
|
||||
)
|
||||
# Lead-time defaults are expressed as offsets FROM the SO's planned-start
|
||||
# date so they track real production schedules, not just "today + N".
|
||||
# If planned_start is unset on the SO, the cascade falls back to today.
|
||||
x_fc_default_internal_deadline_days = fields.Integer(
|
||||
string='Internal Deadline (+ days from start)',
|
||||
help='Pre-fills SO internal deadline as planned_start_date + this '
|
||||
'many days. e.g. 5 means "ship five days after we start".',
|
||||
)
|
||||
x_fc_default_customer_deadline_days = fields.Integer(
|
||||
string='Customer Deadline (+ days from start)',
|
||||
help='Pre-fills the customer-facing commitment date as '
|
||||
'planned_start_date + this many days.',
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
@@ -16,16 +17,70 @@ class SaleOrder(models.Model):
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id_invoice_strategy(self):
|
||||
"""Auto-fill invoice strategy from customer defaults."""
|
||||
if self.partner_id:
|
||||
default = self.env['fp.invoice.strategy.default'].search(
|
||||
[('partner_id', '=', self.partner_id.id)], limit=1,
|
||||
)
|
||||
if default:
|
||||
self.x_fc_invoice_strategy = default.default_strategy
|
||||
self.x_fc_deposit_percent = default.default_deposit_percent
|
||||
if default.payment_term_id:
|
||||
self.payment_term_id = default.payment_term_id
|
||||
"""Auto-fill plating defaults from customer profile.
|
||||
|
||||
Cascade order: partner-level defaults first (the new fast-order
|
||||
path), then fall back to the legacy fp.invoice.strategy.default
|
||||
records for customers migrated before that model was retired.
|
||||
Native Odoo cascades (payment terms, fiscal position) handle
|
||||
themselves via property_* fields and don't need code here.
|
||||
"""
|
||||
if not self.partner_id:
|
||||
return
|
||||
|
||||
partner = self.partner_id
|
||||
|
||||
if partner.x_fc_default_invoice_strategy:
|
||||
self.x_fc_invoice_strategy = partner.x_fc_default_invoice_strategy
|
||||
if partner.x_fc_default_deposit_percent:
|
||||
self.x_fc_deposit_percent = partner.x_fc_default_deposit_percent
|
||||
if partner.x_fc_default_delivery_method:
|
||||
self.x_fc_delivery_method = partner.x_fc_default_delivery_method
|
||||
|
||||
self._fp_recompute_default_deadlines()
|
||||
|
||||
# Legacy fallback: invoice strategy default model. Only fills
|
||||
# gaps left by the partner fields above so a partial migration
|
||||
# doesn't clobber explicit partner-level values.
|
||||
legacy = self.env['fp.invoice.strategy.default'].search(
|
||||
[('partner_id', '=', partner.id)], limit=1,
|
||||
)
|
||||
if legacy:
|
||||
if not self.x_fc_invoice_strategy:
|
||||
self.x_fc_invoice_strategy = legacy.default_strategy
|
||||
if not self.x_fc_deposit_percent:
|
||||
self.x_fc_deposit_percent = legacy.default_deposit_percent
|
||||
if legacy.payment_term_id and not self.payment_term_id:
|
||||
self.payment_term_id = legacy.payment_term_id
|
||||
|
||||
@api.onchange('x_fc_planned_start_date')
|
||||
def _onchange_planned_start_date_deadlines(self):
|
||||
"""Recompute deadlines when planned start changes — without it
|
||||
the partner offsets would only fire on partner_id change."""
|
||||
self._fp_recompute_default_deadlines()
|
||||
|
||||
def _fp_recompute_default_deadlines(self):
|
||||
"""Apply partner deadline offsets relative to planned_start_date.
|
||||
|
||||
Falls back to today when planned_start is unset so the estimator
|
||||
gets a value immediately. Never overwrites a deadline already
|
||||
set by the user (we honour explicit input over auto-fill).
|
||||
"""
|
||||
for order in self:
|
||||
partner = order.partner_id
|
||||
if not partner:
|
||||
continue
|
||||
anchor = order.x_fc_planned_start_date or fields.Date.context_today(order)
|
||||
if (partner.x_fc_default_internal_deadline_days
|
||||
and not order.x_fc_internal_deadline):
|
||||
order.x_fc_internal_deadline = (
|
||||
anchor + timedelta(days=partner.x_fc_default_internal_deadline_days)
|
||||
)
|
||||
if (partner.x_fc_default_customer_deadline_days
|
||||
and not order.commitment_date):
|
||||
order.commitment_date = (
|
||||
anchor + timedelta(days=partner.x_fc_default_customer_deadline_days)
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
"""Override to check account hold + customer PO# and trigger
|
||||
|
||||
@@ -23,7 +23,36 @@
|
||||
</small>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<!-- Single "Plating Defaults" tab — invoice strategy, delivery,
|
||||
deadlines, tax type, payment terms. Set once here, cascades
|
||||
onto every new SO for this customer. -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Plating Defaults" name="fp_plating_defaults_tab"
|
||||
invisible="is_company == False and parent_id"
|
||||
groups="fusion_plating_invoicing.group_fp_accounting">
|
||||
<p class="text-muted">
|
||||
Set defaults once per customer to speed up order entry.
|
||||
These cascade onto every new sale order; the estimator
|
||||
can override per order.
|
||||
</p>
|
||||
<group>
|
||||
<group string="Invoicing">
|
||||
<field name="x_fc_default_invoice_strategy"/>
|
||||
<field name="x_fc_default_deposit_percent"
|
||||
invisible="x_fc_default_invoice_strategy != 'deposit'"/>
|
||||
<field name="property_payment_term_id"/>
|
||||
<field name="property_account_position_id"
|
||||
string="Tax Type (Fiscal Position)"/>
|
||||
</group>
|
||||
<group string="Fulfilment">
|
||||
<field name="x_fc_default_delivery_method"/>
|
||||
<field name="x_fc_default_internal_deadline_days"/>
|
||||
<field name="x_fc_default_customer_deadline_days"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Account Hold" name="account_hold_tab"
|
||||
groups="fusion_plating_invoicing.group_fp_accounting">
|
||||
<group>
|
||||
|
||||
Reference in New Issue
Block a user