[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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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', '>', 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({
|
||||
|
||||
Reference in New Issue
Block a user