feat(invoicing): strategy auto-fill + hold check + deposit/COD automation + menu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 19:36:27 -04:00
parent 10e3ada9e9
commit 7dea212c13
4 changed files with 172 additions and 2 deletions

View File

@@ -2,4 +2,27 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
# Placeholder — implemented in a later task.
from odoo import models, _
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
"""Check account hold before posting invoices."""
for move in self:
if move.move_type in ('out_invoice', 'out_refund') and move.partner_id:
if move.partner_id.x_fc_account_hold:
is_manager = self.env.user.has_group(
'fusion_plating.group_fusion_plating_manager'
)
if not is_manager:
raise UserError(_(
'Cannot post invoice — customer "%s" is on account hold.\n'
'Reason: %s\n\n'
'Contact a manager to override.'
) % (move.partner_id.name,
move.partner_id.x_fc_account_hold_reason or 'No reason specified'))
return super().action_post()

View File

@@ -2,4 +2,105 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
# Placeholder — implemented in a later task.
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
@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
def action_confirm(self):
"""Override to check account hold and trigger invoice strategy."""
for order in self:
# --- Account hold check ---
if order.partner_id.x_fc_account_hold:
is_manager = self.env.user.has_group(
'fusion_plating.group_fusion_plating_manager'
)
if not is_manager:
raise UserError(_(
'Cannot confirm — customer "%s" is on account hold.\n'
'Reason: %s\n\n'
'Contact a manager to override.'
) % (order.partner_id.name,
order.partner_id.x_fc_account_hold_reason or 'No reason specified'))
else:
# Manager gets a warning in chatter but can proceed
order.message_post(
body=_(
'Warning: Customer "%s" is on account hold (reason: %s). '
'Order confirmed by manager override.'
) % (order.partner_id.name,
order.partner_id.x_fc_account_hold_reason or 'N/A'),
)
res = super().action_confirm()
# --- Invoice strategy automation ---
for order in self:
strategy = order.x_fc_invoice_strategy
if not strategy:
continue
if strategy == 'deposit' and order.x_fc_deposit_percent:
order._create_deposit_invoice()
elif strategy == 'cod_prepay':
order._create_full_invoice()
return res
def _create_deposit_invoice(self):
"""Create a deposit (down payment) invoice for the deposit percentage."""
self.ensure_one()
percent = self.x_fc_deposit_percent
if not percent or percent <= 0:
return
try:
# Use Odoo's standard down payment mechanism
wizard = self.env['sale.advance.payment.inv'].create({
'advance_payment_method': 'percentage',
'amount': percent,
})
wizard.with_context(active_ids=self.ids, active_model='sale.order').create_invoices()
self.message_post(
body=_('Deposit invoice (%.0f%%) created automatically — strategy: Deposit.') % percent,
)
except Exception as e:
_logger.warning('Failed to create deposit invoice for SO %s: %s', self.name, e)
self.message_post(
body=_('Failed to auto-create deposit invoice: %s. Create manually.') % str(e),
)
def _create_full_invoice(self):
"""Create a full invoice immediately (COD/Prepay strategy)."""
self.ensure_one()
try:
invoices = self._create_invoices()
if invoices:
self.message_post(
body=_('Full invoice created automatically — strategy: COD / Prepay.'),
)
except Exception as e:
_logger.warning('Failed to create COD invoice for SO %s: %s', self.name, e)
self.message_post(
body=_('Failed to auto-create invoice: %s. Create manually.') % str(e),
)

View File

@@ -5,4 +5,33 @@
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== Window actions (BEFORE menus) ===== -->
<record id="action_fp_account_holds" model="ir.actions.act_window">
<field name="name">Account Holds</field>
<field name="res_model">res.partner</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_account_hold', '=', True)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No customers on account hold
</p>
</field>
</record>
<!-- ===== Menu items under Configuration ===== -->
<menuitem id="menu_fp_invoice_strategy"
name="Invoice Strategy Defaults"
parent="fusion_plating.menu_fp_config"
action="action_fp_invoice_strategy_default"
sequence="60"
groups="group_fp_accounting"/>
<menuitem id="menu_fp_account_holds"
name="Account Holds"
parent="fusion_plating.menu_fp_config"
action="action_fp_account_holds"
sequence="70"
groups="group_fp_accounting"/>
</odoo>

View File

@@ -5,4 +5,21 @@
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== Account hold warning banner on SO form ===== -->
<record id="view_sale_order_form_hold_banner" model="ir.ui.view">
<field name="name">sale.order.form.fp.hold.banner</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//form/header" position="before">
<div class="alert alert-danger" role="alert"
invisible="not partner_id or not partner_id.x_fc_account_hold">
<strong>Account Hold</strong> — This customer is on account hold.
SO confirmation, invoicing, and shipping are blocked for non-managers.
</div>
</xpath>
</field>
</record>
</odoo>