This commit is contained in:
gsinghpal
2026-04-27 00:11:18 -04:00
parent d9f58b9851
commit f08f328688
116 changed files with 9891 additions and 359 deletions

View File

@@ -116,11 +116,61 @@ class FpDirectOrderLine(models.Model):
@api.onchange('part_catalog_id')
def _onchange_part_clears_variant(self):
"""Clear variant pick when the part changes (variants are part-scoped)."""
"""Clear variant pick when the part changes (variants are part-scoped).
Pre-fill coating + treatments from the part's saved defaults so
Sarah doesn't re-pick the same coating every repeat customer.
Defaults only apply when the line currently has no coating set
— editing an existing line with a chosen coating doesn't get
clobbered.
For BRAND-NEW parts (no defaults saved yet) auto-tick
`push_to_defaults` so Sarah's first coating pick gets persisted
back to the part. Without this Sarah has to remember to tick the
toggle herself, and the second order doesn't pre-fill.
Returns a warning popup explaining what's happening.
"""
warning = None
for rec in self:
# Variant clear (original behaviour).
if (rec.process_variant_id
and rec.process_variant_id.part_catalog_id != rec.part_catalog_id):
rec.process_variant_id = False
if not rec.part_catalog_id:
continue
part = rec.part_catalog_id
has_default_coating = bool(getattr(
part, 'x_fc_default_coating_config_id', False))
has_default_treatments = bool(getattr(
part, 'x_fc_default_treatment_ids', False))
# Pre-fill default coating if the line is empty.
if not rec.coating_config_id and has_default_coating:
rec.coating_config_id = part.x_fc_default_coating_config_id
# Pre-fill default treatments if any are configured.
if not rec.treatment_ids and has_default_treatments:
rec.treatment_ids = [(6, 0, part.x_fc_default_treatment_ids.ids)]
# New-part auto-suggest: if neither default exists, this is
# likely a first-time use of the part. Auto-tick the
# push_to_defaults toggle so whatever Sarah picks becomes
# the saved default — surface a warning popup so she knows.
# `is_one_off` always wins (operator opted out of catalog
# persistence), so don't auto-tick in that case.
if (not has_default_coating
and not has_default_treatments
and not rec.is_one_off
and not rec.push_to_defaults):
rec.push_to_defaults = True
warning = {
'title': _('First-Time Part — Defaults Will Be Saved'),
'message': _(
'%(part)s has no saved coating / treatments. '
'The coating + treatments you pick on this line '
'will be saved as the part\'s defaults so the '
'next order auto-fills them. Untick "Save as '
'Default" on the line if you don\'t want this.'
) % {'part': part.display_name or part.part_number or '(part)'},
}
return {'warning': warning} if warning else None
# ---- Qty / price ----
quantity = fields.Integer(string='Qty', default=1, required=True)

View File

@@ -209,37 +209,63 @@ class FpDirectOrderWizard(models.Model):
# ---- Onchange ----
@api.onchange('partner_id')
def _onchange_partner_id(self):
"""Seed invoice defaults + addresses + payment terms when customer changes."""
if self.partner_id and 'x_fc_default_invoice_strategy' in self.partner_id._fields:
self.invoice_strategy = self.partner_id.x_fc_default_invoice_strategy or False
self.deposit_percent = self.partner_id.x_fc_default_deposit_percent or 0.0
if self.partner_id:
addrs = self.partner_id.address_get(['invoice', 'delivery'])
self.partner_invoice_id = addrs.get('invoice') or self.partner_id.id
self.partner_shipping_id = addrs.get('delivery') or self.partner_id.id
# Seed payment terms: customer's invoice-strategy default wins;
# fallback to partner.property_payment_term_id.
term = False
isd = self.env['fp.invoice.strategy.default'].search(
[('partner_id', '=', self.partner_id.id)], limit=1,
)
if isd and isd.payment_term_id:
term = isd.payment_term_id
# Also seed strategy from the same record if not already set.
if not self.invoice_strategy:
self.invoice_strategy = isd.default_strategy
if not self.deposit_percent:
self.deposit_percent = isd.default_deposit_percent or 0.0
if not term and self.partner_id.property_payment_term_id:
term = self.partner_id.property_payment_term_id
self.payment_term_id = term or False
else:
"""Seed invoice defaults + addresses + payment terms when customer
changes. Also surface an account-hold warning so Sarah doesn't
build a full quote for a customer she can't ship to.
"""
if not self.partner_id:
self.partner_invoice_id = False
self.partner_shipping_id = False
self.payment_term_id = False
self._apply_strategy_payment_term()
return
# Legacy partner-field defaults (pre-Sub-5).
if 'x_fc_default_invoice_strategy' in self.partner_id._fields:
self.invoice_strategy = self.partner_id.x_fc_default_invoice_strategy or False
self.deposit_percent = self.partner_id.x_fc_default_deposit_percent or 0.0
# Addresses.
addrs = self.partner_id.address_get(['invoice', 'delivery'])
self.partner_invoice_id = addrs.get('invoice') or self.partner_id.id
self.partner_shipping_id = addrs.get('delivery') or self.partner_id.id
# Per-customer invoice strategy default (fp.invoice.strategy.default).
# Pull strategy + deposit even when payment_term_id is empty — the
# previous condition `if isd and isd.payment_term_id` silently
# skipped the strategy fill for net-terms customers without
# explicit terms configured.
isd = self.env['fp.invoice.strategy.default'].search(
[('partner_id', '=', self.partner_id.id)], limit=1,
)
term = False
if isd:
if not self.invoice_strategy:
self.invoice_strategy = isd.default_strategy
if not self.deposit_percent:
self.deposit_percent = isd.default_deposit_percent or 0.0
term = isd.payment_term_id
if not term and self.partner_id.property_payment_term_id:
term = self.partner_id.property_payment_term_id
self.payment_term_id = term or False
# Re-apply strategy → terms mapping after partner switch.
self._apply_strategy_payment_term()
# Account-hold early warning. Hard block lives in action_confirm
# but Sarah deserves to know NOW before she builds 5 lines.
if getattr(self.partner_id, 'x_fc_account_hold', False):
return {
'warning': {
'title': _('Customer on Account Hold'),
'message': _(
'%s is currently on account hold. You can still '
'build the quotation, but it cannot be confirmed '
'until the hold is cleared by accounting.'
) % self.partner_id.display_name,
}
}
@api.onchange('invoice_strategy')
def _onchange_invoice_strategy(self):
"""Map the strategy onto sensible payment terms."""
@@ -247,12 +273,15 @@ class FpDirectOrderWizard(models.Model):
def _apply_strategy_payment_term(self):
"""Mapping rule:
- cod_prepay → Immediate Payment (Odoo's stock term)
- deposit / progress / net_terms → keep what the partner default
already gave us; if blank, leave it blank so the user can pick.
Never overwrites an explicit user choice for non-COD strategies —
only fills in when payment_term_id is empty.
- cod_prepay → Immediate Payment
- net_terms / deposit / progress → fall back to a 30-day
term when nothing is set. Without ANY payment term Odoo
blocks invoice posting, which silently strands SOs at the
invoicing step. Better to default to net-30 and let the
estimator override if the customer's terms are different.
Never overwrites an explicit user choice — only fills the gap.
"""
Pt = self.env['account.payment.term']
for rec in self:
if rec.invoice_strategy == 'cod_prepay':
immediate = rec.env.ref(
@@ -261,6 +290,20 @@ class FpDirectOrderWizard(models.Model):
)
if immediate:
rec.payment_term_id = immediate.id
elif rec.invoice_strategy in ('net_terms', 'deposit', 'progress') \
and not rec.payment_term_id:
# Try canonical Net-30, then any term named "30 Days",
# then any term at all as last-ditch.
term = rec.env.ref(
'account.account_payment_term_30days',
raise_if_not_found=False,
)
if not term:
term = Pt.search([('name', 'ilike', '30 Days')], limit=1)
if not term:
term = Pt.search([], limit=1)
if term:
rec.payment_term_id = term.id
# ---- Actions ----
@api.model
@@ -351,6 +394,17 @@ class FpDirectOrderWizard(models.Model):
raise UserError(_('Pick a customer before confirming.'))
if not self.line_ids:
raise UserError(_('Add at least one part line before confirming.'))
# Account-hold hard block — same policy as sale.order.action_confirm
# but enforced earlier so the wizard doesn't waste Sarah's time.
# Manager override allowed via context key fp_skip_account_hold=True.
if (getattr(self.partner_id, 'x_fc_account_hold', False)
and not self.env.context.get('fp_skip_account_hold')
and not self.env.user.has_group(
'fusion_plating.group_fusion_plating_manager')):
raise UserError(_(
'Customer %s is on account hold. Have a manager clear the '
'hold (or override) before creating the order.'
) % self.partner_id.display_name)
# Accept EITHER a PO (document + number) OR the PO Pending
# flag. Customers who haven't sent paperwork yet use Pending;

View File

@@ -141,9 +141,9 @@
<field name="is_missing_info" column_invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="part_catalog_id"
context="{'default_partner_id': parent.partner_id}"
context="{'default_partner_id': parent.partner_id, 'default_revision': 'A'}"
domain="[('partner_id', '=', parent.partner_id), ('is_latest_revision', '=', True)]"
options="{'no_create_edit': True}"/>
options="{'no_quick_create': True}"/>
<field name="description_template_id"
domain="[('part_catalog_id', '=', part_catalog_id)]"
context="{'default_part_catalog_id': part_catalog_id}"