[IMP] sale: add confirmed SO to reveivable

The goal is to take into account the confirmed
and un-invoiced sales orders in the total receivable of a client.
It will be useful in the scope of the credit limit trigger.

This is broken down in 3 steps:
1. Add the Remaining Amount to Invoice (RAI) in the
Invoice creation wizard of the Sales Order and store this data.
2. Add the RAI in the Total Receivable of the client.
3. The Remaining amount to invoice goes to optional="hide"
in the list view of "Orders to invoice" list view.

task-3122390

closes odoo/odoo#112905

Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
John Laterre (jol)
2023-03-10 17:54:51 +01:00
parent f18e11d177
commit 7bd93cc64b
9 changed files with 172 additions and 41 deletions
+13 -7
View File
@@ -568,6 +568,7 @@ class AccountMove(models.Model):
compute='_compute_partner_credit_warning',
groups="account.group_account_invoice,account.group_account_readonly",
)
partner_credit = fields.Monetary(compute='_compute_partner_credit')
duplicated_ref_ids = fields.Many2many(comodel_name='account.move', compute='_compute_duplicated_ref_ids')
# used to display the various dates and amount dues on the invoice's PDF
@@ -1378,24 +1379,29 @@ class AccountMove(models.Model):
move.move_type == 'out_invoice' and \
move.company_id.account_use_credit_limit
if show_warning:
amount_total_currency = move.currency_id._convert(move.tax_totals['amount_total'], move.company_currency_id, move.company_id, move.date)
updated_credit = move.partner_id.commercial_partner_id.credit + amount_total_currency
move.partner_credit_warning = self._build_credit_warning_message(move, updated_credit)
move.partner_credit_warning = self._build_credit_warning_message(
move, move.partner_credit, move.tax_totals['amount_total'] > 0.0)
def _build_credit_warning_message(self, record, updated_credit):
''' Build the warning message that will be displayed in a yellow banner on top of the current record
@api.depends('partner_id')
def _compute_partner_credit(self):
for move in self:
move.partner_credit = move.partner_id.commercial_partner_id.credit
def _build_credit_warning_message(self, record, updated_credit, include):
""" Build the warning message that will be displayed in a yellow banner on top of the current record
if the partner exceeds a credit limit (set on the company or the partner itself).
:param record: The record where the warning will appear (Invoice, Sales Order...).
:param updated_credit (float): The partner's updated credit limit including the current record.
:param include (bool): Whether the current record's amount is included in the warning message.
:return (str): The warning message to be showed.
'''
"""
partner_id = record.partner_id.commercial_partner_id
if not partner_id.credit_limit or updated_credit <= partner_id.credit_limit:
return ''
msg = _('%s has reached its Credit Limit of : %s\nTotal amount due ',
partner_id.name,
formatLang(self.env, partner_id.credit_limit, currency_obj=record.company_id.currency_id))
if updated_credit > partner_id.credit:
if include:
msg += _('(including this document) ')
msg += ': %s' % formatLang(self.env, updated_credit, currency_obj=record.company_id.currency_id)
return msg
+19
View File
@@ -145,3 +145,22 @@ class AccountMove(models.Model):
# OVERRIDE
self.ensure_one()
return self.line_ids.sale_line_ids and all(sale_line.is_downpayment for sale_line in self.line_ids.sale_line_ids) or False
@api.depends('line_ids.sale_line_ids.order_id', 'currency_id', 'tax_totals', 'date')
def _compute_partner_credit(self):
super()._compute_partner_credit()
for move in self:
sale_order = move.line_ids.sale_line_ids.order_id
amount_total_currency = move.currency_id._convert(
move.tax_totals['amount_total'],
move.company_currency_id,
move.company_id,
move.date
)
amount_to_invoice_currency = sale_order.currency_id._convert(
sale_order.amount_to_invoice,
move.company_currency_id,
move.company_id,
move.date
)
move.partner_credit += max(amount_total_currency - amount_to_invoice_currency, 0.0)
+8
View File
@@ -54,3 +54,11 @@ class ResPartner(models.Model):
all_child = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
action["domain"] = [("partner_id", "in", all_child.ids)]
return action
def _credit_debit_get(self):
super()._credit_debit_get()
domain = [('partner_id', 'in', self.ids), ('state', 'in', ['sale', 'done'])]
group = self.env['sale.order'].read_group(domain, ['amount_to_invoice'], ['partner_id'])
for res in group:
partner = self.browse(res['partner_id'][0])
partner.credit += res['amount_to_invoice']
+21 -1
View File
@@ -218,6 +218,8 @@ class SaleOrder(models.Model):
amount_untaxed = fields.Monetary(string="Untaxed Amount", store=True, compute='_compute_amounts', tracking=5)
amount_tax = fields.Monetary(string="Taxes", store=True, compute='_compute_amounts')
amount_total = fields.Monetary(string="Total", store=True, compute='_compute_amounts', tracking=4)
amount_to_invoice = fields.Monetary(string="Amount to invoice", store=True, compute='_compute_amount_to_invoice')
amount_invoiced = fields.Monetary(string="Already invoiced", compute='_compute_amount_invoiced')
invoice_count = fields.Integer(string="Invoice Count", compute='_get_invoiced')
invoice_ids = fields.Many2many(
@@ -578,6 +580,24 @@ class SaleOrder(models.Model):
else:
record.tax_country_id = record.company_id.account_fiscal_country_id
@api.depends('invoice_ids.state', 'currency_id', 'amount_total')
def _compute_amount_to_invoice(self):
for order in self:
order.amount_to_invoice = order.amount_total
for invoice in order.invoice_ids.filtered(lambda x: x.state == 'posted'):
invoice_amount_currency = invoice.currency_id._convert(
invoice.tax_totals['amount_total'],
order.currency_id,
invoice.company_id,
invoice.date,
)
order.amount_to_invoice -= invoice_amount_currency
@api.depends('amount_total', 'amount_to_invoice')
def _compute_amount_invoiced(self):
for order in self:
order.amount_invoiced = order.amount_total - order.amount_to_invoice
@api.depends('company_id', 'partner_id', 'amount_total')
def _compute_partner_credit_warning(self):
for order in self:
@@ -588,7 +608,7 @@ class SaleOrder(models.Model):
if show_warning:
updated_credit = order.partner_id.commercial_partner_id.credit + (order.amount_total * order.currency_rate)
order.partner_credit_warning = self.env['account.move']._build_credit_warning_message(
order, updated_credit)
order, updated_credit, order.amount_total > 0.0)
@api.depends('order_line.tax_id', 'order_line.price_unit', 'amount_total', 'amount_untaxed', 'currency_id')
def _compute_tax_totals(self):
@@ -403,3 +403,58 @@ class TestSaleOrderDownPayment(TestSaleCommon):
[self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0, False ],
]
self._assert_invoice_lines_values(invoice.line_ids, expected)
def test_warning_on_invoice_with_credit_limit(self):
# Activate the Credit Limit feature and set a value for partner_a.
self.env.company.account_use_credit_limit = True
self.partner_a.credit_limit = 1000.0
# Create and confirm a SO to reach (but not exceed) partner_a's credit limit.
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'order_line': [Command.create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 1,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': 1000.0,
'tax_id': False,
})]
})
# Check that partner_a's credit is 0.0.
self.assertEqual(self.partner_a.credit, 0.0)
# Make sure partner_a's credit includes the newly confirmed SO.
sale_order.action_confirm()
self.partner_a.invalidate_recordset(['credit'])
self.assertEqual(self.partner_a.credit, 1000.0)
# Create a 50% down payment invoice.
self.env['sale.advance.payment.inv'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order.id],
'active_id': sale_order.id,
'default_journal_id': self.company_data['default_journal_sale'].id,
}).create({
'advance_payment_method': 'percentage',
'amount': 50,
'deposit_account_id': self.revenue_account.id,
}).create_invoices()
# Check that the warning does not appear even though we are creating an invoice
# that should bring partner_a's credit above its limit.
self.assertEqual(sale_order.invoice_ids.partner_credit_warning, '')
# Make the down payment invoice amount larger than the Amount to Invoice
# and check that the warning appears with the correct amounts,
# i.e. 1.500 instead of 2.500 (1.000 SO + 1.500 down payment invoice).
sale_order.invoice_ids.invoice_line_ids.quantity = 3
self.assertEqual(
sale_order.invoice_ids.partner_credit_warning,
"partner_a has reached its Credit Limit of : $\xa01,000.00\n"
"Total amount due (including this document) : $\xa01,500.00"
)
+1
View File
@@ -132,6 +132,7 @@
decoration-warning="invoice_status == 'upselling'"
widget="badge" optional="show"/>
<field name="tag_ids" optional="hide" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="amount_to_invoice" optional="hide"/>
<field name="state" invisible="1"/>
</tree>
</field>
@@ -55,6 +55,14 @@ class SaleAdvancePaymentInv(models.TransientModel):
comodel_name='res.company',
compute='_compute_company_id',
store=True)
amount_invoiced = fields.Monetary(
string="Already invoiced",
compute="_compute_invoice_amounts",
help="Only confirmed down payments are considered.")
amount_to_invoice = fields.Monetary(
string="Amount to invoice",
compute="_compute_invoice_amounts",
help="The amount to invoice = Sale Order Total - Confirmed Down Payments.")
# Only used when there is no down payment product available
# to setup the down payment product
@@ -71,6 +79,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
# UI
display_draft_invoice_warning = fields.Boolean(compute="_compute_display_draft_invoice_warning")
display_invoice_amount_warning = fields.Boolean(compute="_compute_display_invoice_amount_warning")
#=== COMPUTE METHODS ===#
@@ -109,13 +118,24 @@ class SaleAdvancePaymentInv(models.TransientModel):
if wizard.count == 1:
wizard.product_id = wizard.company_id.sale_down_payment_product_id
@api.depends('sale_order_ids', 'advance_payment_method', 'deduct_down_payments')
@api.depends('amount', 'fixed_amount', 'advance_payment_method', 'amount_to_invoice')
def _compute_display_invoice_amount_warning(self):
for wizard in self:
invoice_amount = wizard.fixed_amount
if wizard.advance_payment_method == 'percentage':
invoice_amount = wizard.amount / 100 * sum(wizard.sale_order_ids.mapped('amount_total'))
wizard.display_invoice_amount_warning = invoice_amount > wizard.amount_to_invoice
@api.depends('sale_order_ids')
def _compute_display_draft_invoice_warning(self):
for wizard in self:
wizard.display_draft_invoice_warning = (
wizard.advance_payment_method == 'delivered' and
any(aml.parent_state == 'draft' for aml in wizard.sale_order_ids.order_line.invoice_lines)
)
wizard.display_draft_invoice_warning = wizard.sale_order_ids.invoice_ids.filtered(lambda invoice: invoice.state == 'draft')
@api.depends('sale_order_ids')
def _compute_invoice_amounts(self):
for wizard in self:
wizard.amount_invoiced = sum(wizard.sale_order_ids.mapped('amount_invoiced'))
wizard.amount_to_invoice = sum(wizard.sale_order_ids.mapped('amount_to_invoice'))
#=== ONCHANGE METHODS ===#
@@ -127,7 +147,6 @@ class SaleAdvancePaymentInv(models.TransientModel):
#=== CONSTRAINT METHODS ===#
@api.constrains('advance_payment_method', 'amount', 'fixed_amount')
def _check_amount_is_positive(self):
for wizard in self:
if wizard.advance_payment_method == 'percentage' and wizard.amount <= 0.00:
@@ -153,11 +172,19 @@ class SaleAdvancePaymentInv(models.TransientModel):
#=== ACTION METHODS ===#
def create_invoices(self):
self._check_amount_is_positive()
invoices = self._create_invoices(self.sale_order_ids)
if self.env.context.get('open_invoices'):
return self.sale_order_ids.action_view_invoice(invoices=invoices)
return self.sale_order_ids.action_view_invoice(invoices=invoices)
return {'type': 'ir.actions.act_window_close'}
def view_draft_invoices(self):
return {
'name': _('Draft Invoices'),
'type': 'ir.actions.act_window',
'view_mode': 'tree',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'account.move',
'domain': [('line_ids.sale_line_ids.order_id', 'in', self.sale_order_ids.ids), ('state', '=', 'draft')],
}
#=== BUSINESS METHODS ===#
@@ -6,29 +6,20 @@
<field name="model">sale.advance.payment.inv</field>
<field name="arch" type="xml">
<form string="Invoice Sales Order">
<p class="oe_grey">
Invoices will be created in draft so that you can review
them before validation.
</p>
<field name="display_draft_invoice_warning" invisible="1"/>
<p class="alert alert-warning" role="alert" attrs="{'invisible': [('display_draft_invoice_warning', '=', False)]}">
The new invoice will deduct draft invoices linked to this sale order.
</p>
<field name="has_down_payments" invisible="1"/>
<div class="alert alert-warning pb-1" role="alert" attrs="{'invisible': [('display_draft_invoice_warning', '=', False)]}">
<p>There are existing <a name="view_draft_invoices" type="object">Draft Invoices</a> for this Sale Order.</p>
<p attrs="{'invisible': [('advance_payment_method', '!=', 'delivered')]}">
The new invoice will deduct draft invoices linked to this sale order.
</p>
</div>
<group>
<field name="sale_order_ids" invisible="1"/>
<field name="has_down_payments" invisible="1"/>
<field name="count" attrs="{'invisible': [('count', '=', 1)]}"/>
<field name="advance_payment_method" class="oe_inline"
widget="radio"
attrs="{'invisible': [('count', '&gt;', 1)]}"/>
<label for="deduct_down_payments"
string=""
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
<div attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
id="down_payment_details">
<field name="deduct_down_payments" nolabel="1"/>
<label for="deduct_down_payments"/>
</div>
</group>
<group name="down_payment_specification"
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed', 'percentage'))]}">
@@ -37,6 +28,7 @@
<label for="amount"/>
<div id="payment_method_details">
<field name="currency_id" invisible="1"/>
<field name="display_invoice_amount_warning" invisible="1"/>
<field name="fixed_amount"
attrs="{'required': [('advance_payment_method', '=', 'fixed')],
'invisible': [('advance_payment_method', '!=', 'fixed')]}"
@@ -46,7 +38,12 @@
'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline"/>
<span attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline">%</span>
class="oe_inline">% </span>
<span attrs="{'invisible': [('display_invoice_amount_warning', '=', False)]}"
class="oe_inline text-danger"
title="The Down Payment is greater than the amount remaining to be invoiced.">
<i class="fa fa-warning"/>
</span>
</div>
<field name="deposit_account_id"
options="{'no_create': True}"
@@ -56,16 +53,15 @@
widget="many2many_tags"
attrs="{'invisible': [('product_id', '!=', False)]}"/>
</group>
<group attrs="{'invisible': [('has_down_payments', '=', False)]}">
<field name="amount_invoiced"/>
<field name="amount_to_invoice"/>
</group>
<footer>
<button name="create_invoices" type="object"
id="create_invoice_open"
string="Create and View Invoice"
context="{'open_invoices': True}"
string="Create Draft Invoice"
class="btn-primary" data-hotkey="q"/>
<button name="create_invoices" type="object"
id="create_invoice"
string="Create Invoice"
data-hotkey="w"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
@@ -332,7 +332,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
"active_model": 'sale.order',
"active_ids": [sale_order.id],
"active_id": sale_order.id,
'open_invoices': True,
}
# Invoice the 1
wizard = self.env['sale.advance.payment.inv'].with_context(context).create({