diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 6cf6ed74e03..ecb36dd558c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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 diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index 467eeea5084..0087647235a 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -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) diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py index b143a05b41a..30ec3525427 100644 --- a/addons/sale/models/res_partner.py +++ b/addons/sale/models/res_partner.py @@ -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'] diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 8c05e855459..d583ee1a054 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -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): diff --git a/addons/sale/tests/test_sale_order_down_payment.py b/addons/sale/tests/test_sale_order_down_payment.py index e23aec7c2e5..eae07013b1d 100644 --- a/addons/sale/tests/test_sale_order_down_payment.py +++ b/addons/sale/tests/test_sale_order_down_payment.py @@ -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" + ) diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index f3a86504889..b155e664b13 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -132,6 +132,7 @@ decoration-warning="invoice_status == 'upselling'" widget="badge" optional="show"/> + diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 206041fd791..a061788b79c 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -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 ===# diff --git a/addons/sale/wizard/sale_make_invoice_advance_views.xml b/addons/sale/wizard/sale_make_invoice_advance_views.xml index 97784ecfd06..1b6cb6e28ca 100644 --- a/addons/sale/wizard/sale_make_invoice_advance_views.xml +++ b/addons/sale/wizard/sale_make_invoice_advance_views.xml @@ -6,29 +6,20 @@ sale.advance.payment.inv
-

- Invoices will be created in draft so that you can review - them before validation. -

- + + - - @@ -37,6 +28,7 @@ + + + + diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 471b84b9b4f..d10df27b5fc 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -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({