From aa9dcc2bce2002ef62bfd784cc94a4762f3253cf Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Fri, 12 Aug 2022 15:05:19 +0000 Subject: [PATCH] [REF] sale,*: invoicing wizard 1) Invoicing wizard refactoring * reduce the use of context in the business logic, store the orders being invoiced * factorize invoice creation values, using the correct method from the sale.order model * switch from default fields to computed fields, and avoid computing unnecessary fields when the wizard is opened to invoice multiple orders together (most fields are only used for down payments) * Rely on python constraints instead of error messages scattered accross business methods to notify the user that the invoice cannot be created. * Clean the code to make it easier to follow, understand and maintain 2) Distinguish Down Payment Invoice lines from standard ones (with a dedicated field) Allows to stop relying on the dumb property, not specifically reliable, e.g. if the down payment product is modified in the settings after down payments orders were already created. closes odoo/odoo#98600 Related: odoo/enterprise#30653 Related: odoo/upgrade#3799 Signed-off-by: Victor Feyens (vfe) --- addons/l10n_id_efaktur/models/account_move.py | 5 +- .../wizard/sale_make_invoice_advance.py | 4 +- addons/sale/models/account_move_line.py | 1 + addons/sale/models/sale_order.py | 10 +- addons/sale/models/sale_order_line.py | 13 +- addons/sale/tests/test_sale_refund.py | 26 +- .../sale/wizard/sale_make_invoice_advance.py | 384 ++++++++++-------- .../sale_make_invoice_advance_views.xml | 135 +++--- .../wizard/sale_make_invoice_advance.py | 35 +- .../sale_make_invoice_advance_views.xml | 6 +- 10 files changed, 360 insertions(+), 259 deletions(-) diff --git a/addons/l10n_id_efaktur/models/account_move.py b/addons/l10n_id_efaktur/models/account_move.py index 7ffe407aeed..d810e77a13b 100644 --- a/addons/l10n_id_efaktur/models/account_move.py +++ b/addons/l10n_id_efaktur/models/account_move.py @@ -136,9 +136,6 @@ class AccountMove(models.Model): def _generate_efaktur_invoice(self, delimiter): """Generate E-Faktur for customer invoice.""" # Invoice of Customer - company_id = self.company_id - dp_product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') - output_head = '%s%s%s' % ( _csv_row(FK_HEAD_LIST, delimiter), _csv_row(LT_HEAD_LIST, delimiter), @@ -180,7 +177,7 @@ class AccountMove(models.Model): eTax['REFERENSI'] = number_ref eTax['KODE_DOKUMEN_PENDUKUNG'] = '0' - lines = move.line_ids.filtered(lambda x: x.product_id.id == int(dp_product_id) and x.price_unit < 0 and x.display_type == 'product') + lines = move.line_ids.filtered(lambda x: x.is_downpayment and x.price_unit < 0 and not x.display_type) eTax['FG_UANG_MUKA'] = 0 eTax['UANG_MUKA_DPP'] = int(abs(sum(lines.mapped(lambda l: float_round(l.price_subtotal, 0))))) eTax['UANG_MUKA_PPN'] = int(abs(sum(lines.mapped(lambda l: float_round(l.price_total - l.price_subtotal, 0))))) diff --git a/addons/l10n_in_sale/wizard/sale_make_invoice_advance.py b/addons/l10n_in_sale/wizard/sale_make_invoice_advance.py index 6fc5f349c04..f22ba445d1b 100644 --- a/addons/l10n_in_sale/wizard/sale_make_invoice_advance.py +++ b/addons/l10n_in_sale/wizard/sale_make_invoice_advance.py @@ -7,8 +7,8 @@ from odoo import models class SaleAdvancePaymentInv(models.TransientModel): _inherit = "sale.advance.payment.inv" - def _prepare_invoice_values(self, order, name, amount, so_line): - res = super()._prepare_invoice_values(order, name, amount, so_line) + def _prepare_invoice_values(self, order, so_line): + res = super()._prepare_invoice_values(order, so_line) if order.l10n_in_journal_id: res['journal_id'] = order.l10n_in_journal_id.id if order.country_code == 'IN': diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 413abc9265d..6cb4f6d4e35 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -9,6 +9,7 @@ from odoo.tools import float_compare, float_is_zero class AccountMoveLine(models.Model): _inherit = 'account.move.line' + is_downpayment = fields.Boolean() sale_line_ids = fields.Many2many( 'sale.order.line', 'sale_order_line_invoice_rel', diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 3e48e983f1d..1d6e480bb68 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -947,28 +947,26 @@ class SaleOrder(models.Model): """ self.ensure_one() - invoice_vals = { + return { 'ref': self.client_order_ref or '', 'move_type': 'out_invoice', 'narration': self.note, - 'currency_id': self.pricelist_id.currency_id.id, + 'currency_id': self.currency_id.id, 'campaign_id': self.campaign_id.id, 'medium_id': self.medium_id.id, 'source_id': self.source_id.id, - 'user_id': self.user_id.id, - 'invoice_user_id': self.user_id.id, 'team_id': self.team_id.id, 'partner_id': self.partner_invoice_id.id, 'partner_shipping_id': self.partner_shipping_id.id, 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id._get_fiscal_position(self.partner_invoice_id)).id, 'invoice_origin': self.name, 'invoice_payment_term_id': self.payment_term_id.id, + 'invoice_user_id': self.user_id.id, 'payment_reference': self.reference, 'transaction_ids': [Command.set(self.transaction_ids.ids)], - 'invoice_line_ids': [], 'company_id': self.company_id.id, + 'invoice_line_ids': [], } - return invoice_vals def action_view_invoice(self): invoices = self.mapped('invoice_ids') diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index fad44e22147..ea6b1532503 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -5,6 +5,7 @@ from datetime import timedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.fields import Command from odoo.osv import expression from odoo.tools import float_is_zero, float_compare, float_round @@ -1043,11 +1044,10 @@ class SaleOrderLine(models.Model): return new or old def _prepare_invoice_line(self, **optional_values): - """ - Prepare the dict of values to create the new invoice line for a sales order line. + """Prepare the values to create the new invoice line for a sales order line. - :param qty: float quantity to invoice :param optional_values: any parameter that should be added to the returned invoice line + :rtype: dict """ self.ensure_one() res = { @@ -1059,9 +1059,10 @@ class SaleOrderLine(models.Model): 'quantity': self.qty_to_invoice, 'discount': self.discount, 'price_unit': self.price_unit, - 'tax_ids': [(6, 0, self.tax_id.ids)], - 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], - 'sale_line_ids': [(4, self.id)], + 'tax_ids': [Command.set(self.tax_id.ids)], + 'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)], + 'sale_line_ids': [Command.link(self.id)], + 'is_downpayment': self.is_downpayment, } if self.order_id.analytic_account_id: res['analytic_account_id'] = self.order_id.analytic_account_id.id diff --git a/addons/sale/tests/test_sale_refund.py b/addons/sale/tests/test_sale_refund.py index a20964bc885..ccb0b6c2fc5 100644 --- a/addons/sale/tests/test_sale_refund.py +++ b/addons/sale/tests/test_sale_refund.py @@ -329,7 +329,19 @@ class TestSaleRefund(TestSaleCommon): 'tax_id': False, }) + self.assertRecordValues(sol_product, [{ + 'price_unit': 280.0, + 'discount': 0.0, + 'product_uom_qty': 5.0, + 'qty_to_invoice': 0.0, + 'invoice_status': 'no', + }]) + sale_order_refund.action_confirm() + + self.assertEqual(sol_product.qty_to_invoice, 5.0) + self.assertEqual(sol_product.invoice_status, 'to invoice') + so_context = { 'active_model': 'sale.order', 'active_ids': [sale_order_refund.id], @@ -343,9 +355,21 @@ class TestSaleRefund(TestSaleCommon): 'deposit_account_id': self.company_data['default_account_revenue'].id }) downpayment.create_invoices() - sale_order_refund.invoice_ids[0].action_post() # order_line[1] is the down payment section sol_downpayment = sale_order_refund.order_line[2] + dp_invoice = sale_order_refund.invoice_ids[0] + dp_invoice.action_post() + + self.assertRecordValues(sol_downpayment, [{ + 'price_unit': 700.0, + 'discount': 0.0, + 'invoice_status': 'to invoice', + 'untaxed_amount_to_invoice': -700.0, + 'untaxed_amount_invoiced': 700.0, + 'product_uom_qty': 0.0, + 'qty_invoiced': 1.0, + 'qty_to_invoice': -1.0, + }]) payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({ 'deposit_account_id': self.company_data['default_account_revenue'].id diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 04eb414df79..dd557d6f7a8 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -5,138 +5,205 @@ import time from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.fields import Command class SaleAdvancePaymentInv(models.TransientModel): - _name = "sale.advance.payment.inv" + _name = 'sale.advance.payment.inv' _description = "Sales Advance Payment Invoice" - @api.model - def _count(self): - return len(self._context.get('active_ids', [])) + advance_payment_method = fields.Selection( + selection=[ + ('delivered', "Regular invoice"), + ('percentage', "Down payment (percentage)"), + ('fixed', "Down payment (fixed amount)"), + ], + string="Create Invoice", + default='delivered', + required=True, + help="A standard invoice is issued with all the order lines ready for invoicing," + "according to their invoicing policy (based on ordered or delivered quantity).") + count = fields.Integer(string="Order Count", compute='_compute_count') + sale_order_ids = fields.Many2many( + 'sale.order', default=lambda self: self.env.context.get('active_ids')) - @api.model - def _default_product_id(self): - product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') - return self.env['product.product'].browse(int(product_id)).exists() + # Down Payment logic + has_down_payments = fields.Boolean( + string="Has down payments", compute="_compute_has_down_payments") + deduct_down_payments = fields.Boolean(string="Deduct down payments", default=True) - @api.model - def _default_deposit_account_id(self): - return self._default_product_id()._get_product_accounts()['income'] + # New Down Payment + product_id = fields.Many2one( + comodel_name='product.product', + string="Down Payment Product", + domain=[('type', '=', 'service')], + compute='_compute_product_id', + readonly=False, + store=True) + amount = fields.Float( + string="Down Payment Amount", + help="The percentage of amount to be invoiced in advance, taxes excluded.") + fixed_amount = fields.Monetary( + string="Down Payment Amount (Fixed)", + help="The fixed amount to be invoiced in advance, taxes excluded.") + currency_id = fields.Many2one( + comodel_name='res.currency', + compute='_compute_currency_id', + store=True) + company_id = fields.Many2one( + comodel_name='res.company', + compute='_compute_company_id', + store=True) - @api.model - def _default_deposit_taxes_id(self): - return self._default_product_id().taxes_id + # Only used when there is no down payment product available + # to setup the down payment product + deposit_account_id = fields.Many2one( + comodel_name='account.account', + string="Income Account", + domain=[('deprecated', '=', False)], + help="Account used for deposits") + deposit_taxes_id = fields.Many2many( + comodel_name='account.tax', + string="Customer Taxes", + domain=[('type_tax_use', '=', 'sale')], + help="Taxes used for deposits") - @api.model - def _default_has_down_payment(self): - if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): - sale_order = self.env['sale.order'].browse(self._context.get('active_id')) - return sale_order.order_line.filtered( - lambda sale_order_line: sale_order_line.is_downpayment + #=== COMPUTE METHODS ===# + + @api.depends('sale_order_ids') + def _compute_count(self): + for wizard in self: + wizard.count = len(wizard.sale_order_ids) + + @api.depends('sale_order_ids') + def _compute_has_down_payments(self): + for wizard in self: + wizard.has_down_payments = bool( + wizard.sale_order_ids.order_line.filtered('is_downpayment') ) - return False + # next computed fields are only used for down payments invoices and therefore should only + # have a value when 1 unique SO is invoiced through the wizard + @api.depends('sale_order_ids') + def _compute_currency_id(self): + self.currency_id = False + for wizard in self: + if wizard.count == 1: + wizard.currency_id = wizard.sale_order_ids.currency_id - @api.model - def _default_currency_id(self): - if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): - sale_order = self.env['sale.order'].browse(self._context.get('active_id')) - return sale_order.currency_id + @api.depends('sale_order_ids') + def _compute_company_id(self): + self.company_id = False + for wizard in self: + if wizard.count == 1: + wizard.company_id = wizard.sale_order_ids.company_id - advance_payment_method = fields.Selection([ - ('delivered', 'Regular invoice'), - ('percentage', 'Down payment (percentage)'), - ('fixed', 'Down payment (fixed amount)') - ], string='Create Invoice', default='delivered', required=True, - help="A standard invoice is issued with all the order lines ready for invoicing, \ - according to their invoicing policy (based on ordered or delivered quantity).") - deduct_down_payments = fields.Boolean('Deduct down payments', default=True) - has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True) - product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')], - default=_default_product_id) - count = fields.Integer(default=_count, string='Order Count') - amount = fields.Float('Down Payment Amount', digits='Account', help="The percentage of amount to be invoiced in advance, taxes excluded.") - currency_id = fields.Many2one('res.currency', string='Currency', default=_default_currency_id) - fixed_amount = fields.Monetary('Down Payment Amount (Fixed)', help="The fixed amount to be invoiced in advance, taxes excluded.") - deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], - help="Account used for deposits", default=_default_deposit_account_id) - deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) + @api.depends('company_id') # 'dumb' depends to trigger the computation + def _compute_product_id(self): + self.product_id = False + dp_product_id = int(self.env['ir.config_parameter'].sudo().get_param( + 'sale.default_deposit_product_id')) + if not dp_product_id: + return + for wizard in self: + if wizard.count == 1: + wizard.product_id = dp_product_id + + #=== ONCHANGE METHODS ===# @api.onchange('advance_payment_method') - def onchange_advance_payment_method(self): + def _onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': amount = self.default_get(['amount']).get('amount') return {'value': {'amount': amount}} - return {} - def _prepare_invoice_values(self, order, name, amount, so_line): - invoice_vals = { - 'ref': order.client_order_ref, - 'move_type': 'out_invoice', - 'invoice_origin': order.name, - 'invoice_user_id': order.user_id.id, - 'narration': order.note, - 'partner_id': order.partner_invoice_id.id, - 'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)).id, - 'partner_shipping_id': order.partner_shipping_id.id, - 'currency_id': order.pricelist_id.currency_id.id, - 'payment_reference': order.reference, - 'invoice_payment_term_id': order.payment_term_id.id, - 'partner_bank_id': order.company_id.partner_id.bank_ids[:1].id, - 'team_id': order.team_id.id, - 'campaign_id': order.campaign_id.id, - 'medium_id': order.medium_id.id, - 'source_id': order.source_id.id, - 'invoice_line_ids': [(0, 0, { - 'name': name, - 'price_unit': amount, - 'quantity': 1.0, - 'product_id': self.product_id.id, - 'product_uom_id': so_line.product_uom.id, - 'tax_ids': [(6, 0, so_line.tax_id.ids)], - 'sale_line_ids': [(6, 0, [so_line.id])], - 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)], - 'analytic_account_id': order.analytic_account_id.id or False, - })], + #=== 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: + raise UserError(_('The value of the down payment amount must be positive.')) + elif wizard.advance_payment_method == 'fixed' and wizard.fixed_amount <= 0.00: + raise UserError(_('The value of the down payment amount must be positive.')) + + @api.constrains('product_id') + def _check_down_payment_product_is_valid(self): + for wizard in self: + if wizard.count > 1 or not wizard.product_id: + continue + if wizard.product_id.invoice_policy != 'order': + raise UserError(_( + "The product used to invoice a down payment should have an invoice policy" + "set to \"Ordered quantities\"." + " Please update your deposit product to be able to create a deposit invoice.")) + if wizard.product_id.type != 'service': + raise UserError(_( + "The product used to invoice a down payment should be of type 'Service'." + " Please use another product or update this product.")) + + #=== ACTION METHODS ===# + + def create_invoices(self): + self._create_invoices(self.sale_order_ids) + + if self.env.context.get('open_invoices'): + return self.sale_order_ids.action_view_invoice() + + return {'type': 'ir.actions.act_window_close'} + + #=== BUSINESS METHODS ===# + + def _create_invoices(self, sale_orders): + self.ensure_one() + if self.advance_payment_method == 'delivered': + return sale_orders._create_invoices(final=self.deduct_down_payments) + else: + self.sale_order_ids.ensure_one() + self = self.with_company(self.company_id) + order = self.sale_order_ids + + # Create deposit product if necessary + if not self.product_id: + self.product_id = self.env['product.product'].create( + self._prepare_down_payment_product_values() + ) + self.env['ir.config_parameter'].sudo().set_param( + 'sale.default_deposit_product_id', self.product_id.id) + + # Create down payment section if necessary + if not any(line.display_type and line.is_downpayment for line in order.order_line): + self.env['sale.order.line'].create( + self._prepare_down_payment_section_values(order) + ) + + down_payment_so_line = self.env['sale.order.line'].create( + self._prepare_so_line_values(order) + ) + + invoice = self.env['account.move'].sudo().create( + self._prepare_invoice_values(order, down_payment_so_line) + ).with_user(self.env.uid) # Unsudo the invoice after creation + + invoice.message_post_with_view( + 'mail.message_origin_link', + values={'self': invoice, 'origin': order}, + subtype_id=self.env.ref('mail.mt_note').id) + + return invoice + + def _prepare_down_payment_product_values(self): + self.ensure_one() + return { + 'name': _('Down payment'), + 'type': 'service', + 'invoice_policy': 'order', + 'company_id': False, + 'property_account_income_id': self.deposit_account_id.id, + 'taxes_id': [Command.set(self.deposit_taxes_id.ids)], } - return invoice_vals - - def _get_advance_details(self, order): - context = {'lang': order.partner_id.lang} - if self.advance_payment_method == 'percentage': - if all(self.product_id.taxes_id.mapped('price_include')): - amount = order.amount_total * self.amount / 100 - else: - amount = order.amount_untaxed * self.amount / 100 - name = _("Down payment of %s%%") % (self.amount) - else: - amount = self.fixed_amount - name = _('Down Payment') - del context - - return amount, name - - def _create_invoice(self, order, so_line, amount): - if (self.advance_payment_method == 'percentage' and self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and self.fixed_amount <= 0.00): - raise UserError(_('The value of the down payment amount must be positive.')) - - amount, name = self._get_advance_details(order) - - invoice_vals = self._prepare_invoice_values(order, name, amount, so_line) - - if order.fiscal_position_id: - invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id - - invoice = self.env['account.move'].with_company(order.company_id)\ - .sudo().create(invoice_vals).with_user(self.env.uid) - invoice.message_post_with_view('mail.message_origin_link', - values={'self': invoice, 'origin': order}, - subtype_id=self.env.ref('mail.mt_note').id) - return invoice - - def _prepare_downpayment_section(self, order): + def _prepare_down_payment_section_values(self, order): context = {'lang': order.partner_id.lang} so_values = { @@ -151,72 +218,59 @@ class SaleAdvancePaymentInv(models.TransientModel): del context return so_values - def _prepare_so_line(self, order, analytic_tag_ids, tax_ids, amount): + def _prepare_so_line_values(self, order): + self.ensure_one() context = {'lang': order.partner_id.lang} so_values = { - 'name': _('Down Payment: %s (Draft)') % (time.strftime('%m %Y'),), - 'price_unit': amount, + 'name': _('Down Payment: %s (Draft)', time.strftime('%m %Y')), + 'price_unit': self._get_down_payment_amount(order), 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, - 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, - 'analytic_tag_ids': analytic_tag_ids, - 'tax_id': [(6, 0, tax_ids)], + 'analytic_tag_ids': [ + Command.link(analytic_tag.id) + for line in order.order_line + for analytic_tag in line.analytic_tag_ids + ], 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values - def _create_invoices(self, sale_orders): - if self.advance_payment_method == 'delivered': - return sale_orders._create_invoices(final=self.deduct_down_payments) - else: - # Create deposit product if necessary - if not self.product_id: - vals = self._prepare_deposit_product() - self.product_id = self.env['product.product'].create(vals) - self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id) + def _get_down_payment_amount(self, order): + self.ensure_one() + if self.advance_payment_method == 'percentage': + if all(self.product_id.taxes_id.mapped('price_include')): + amount = order.amount_total * self.amount / 100 + else: + amount = order.amount_untaxed * self.amount / 100 + else: # Fixed amount + amount = self.fixed_amount + return amount - sale_line_obj = self.env['sale.order.line'] - invoices = self.env['account.move'] - for order in sale_orders: - amount, name = self._get_advance_details(order) - - if self.product_id.invoice_policy != 'order': - raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) - if self.product_id.type != 'service': - raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) - taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) - tax_ids = order.fiscal_position_id.map_tax(taxes).ids - analytic_tag_ids = [] - for line in order.order_line: - analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids] - - # verify that no down payment section already exists - if not any(line.display_type and line.is_downpayment for line in order.order_line): - so_downpayment_section_values = self._prepare_downpayment_section(order) - sale_line_obj.create(so_downpayment_section_values) - - so_line_values = self._prepare_so_line(order, analytic_tag_ids, tax_ids, amount) - so_line = sale_line_obj.create(so_line_values) - invoices += self._create_invoice(order, so_line, amount) - return invoices - - def create_invoices(self): - sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', [])) - self._create_invoices(sale_orders) - if self._context.get('open_invoices', False): - return sale_orders.action_view_invoice() - return {'type': 'ir.actions.act_window_close'} - - def _prepare_deposit_product(self): + def _prepare_invoice_values(self, order, so_line): + self.ensure_one() return { - 'name': _('Down payment'), - 'type': 'service', - 'invoice_policy': 'order', - 'property_account_income_id': self.deposit_account_id.id, - 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], - 'company_id': False, + **order._prepare_invoice(), + 'invoice_line_ids': [ + Command.create( + so_line._prepare_invoice_line( + name=self._get_down_payment_description(order), + quantity=1.0, + ) + ) + ], } + + def _get_down_payment_description(self, order): + self.ensure_one() + context = {'lang': order.partner_id.lang} + if self.advance_payment_method == 'percentage': + name = _("Down payment of %s%%", self.amount) + else: + name = _('Down Payment') + del context + + return name diff --git a/addons/sale/wizard/sale_make_invoice_advance_views.xml b/addons/sale/wizard/sale_make_invoice_advance_views.xml index 6cd67ddb172..48a58c4742e 100644 --- a/addons/sale/wizard/sale_make_invoice_advance_views.xml +++ b/addons/sale/wizard/sale_make_invoice_advance_views.xml @@ -1,64 +1,81 @@ - - Invoice Orders - sale.advance.payment.inv - -
-

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

- - - - - -
-
-
-
-
- - Create invoices - ir.actions.act_window - sale.advance.payment.inv - form - new - - - list - + + Invoice Orders + sale.advance.payment.inv + +
+

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

+ + + + + + + + + + +
+
+
+
+
+ + + Create invoices + ir.actions.act_window + sale.advance.payment.inv + form + new + + list +
diff --git a/addons/sale_timesheet/wizard/sale_make_invoice_advance.py b/addons/sale_timesheet/wizard/sale_make_invoice_advance.py index f48b4487387..967ed5859f1 100644 --- a/addons/sale_timesheet/wizard/sale_make_invoice_advance.py +++ b/addons/sale_timesheet/wizard/sale_make_invoice_advance.py @@ -5,24 +5,30 @@ from odoo import api, fields, models class SaleAdvancePaymentInv(models.TransientModel): - _inherit = "sale.advance.payment.inv" - - @api.model - def _default_invoicing_timesheet_enabled(self): - if 'active_id' not in self._context and 'active_ids' not in self._context: - return False - sale_orders = self.env['sale.order'].browse(self._context.get('active_id') or self._context.get('active_ids')) - order_lines = sale_orders.mapped('order_line').filtered(lambda sol: sol.invoice_status == 'to invoice') - product_ids = order_lines.mapped('product_id').filtered(lambda p: p._is_delivered_timesheet()) - return bool(product_ids) + _inherit = 'sale.advance.payment.inv' date_start_invoice_timesheet = fields.Date( - string='Start Date', + string="Start Date", help="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction.") date_end_invoice_timesheet = fields.Date( - string='End Date', + string="End Date", help="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction.") - invoicing_timesheet_enabled = fields.Boolean(default=_default_invoicing_timesheet_enabled) + invoicing_timesheet_enabled = fields.Boolean(compute='_compute_invoicing_timesheet_enabled', store=True) + + #=== COMPUTE METHODS ===# + + @api.depends('sale_order_ids') + def _compute_invoicing_timesheet_enabled(self): + for wizard in self: + wizard.invoicing_timesheet_enabled = bool( + wizard.sale_order_ids.order_line.filtered( + lambda sol: sol.invoice_status == 'to invoice' + ).product_id.filtered( + lambda p: p._is_delivered_timesheet() + ) + ) + + #=== BUSINESS METHODS ===# def _create_invoices(self, sale_orders): """ Override method from sale/wizard/sale_make_invoice_advance.py @@ -34,7 +40,8 @@ class SaleAdvancePaymentInv(models.TransientModel): """ if self.advance_payment_method == 'delivered' and self.invoicing_timesheet_enabled: if self.date_start_invoice_timesheet or self.date_end_invoice_timesheet: - sale_orders.mapped('order_line')._recompute_qty_to_invoice(self.date_start_invoice_timesheet, self.date_end_invoice_timesheet) + sale_orders.order_line._recompute_qty_to_invoice( + self.date_start_invoice_timesheet, self.date_end_invoice_timesheet) return sale_orders.with_context( timesheet_start_date=self.date_start_invoice_timesheet, diff --git a/addons/sale_timesheet/wizard/sale_make_invoice_advance_views.xml b/addons/sale_timesheet/wizard/sale_make_invoice_advance_views.xml index 0090accb3e1..d0f2633992c 100644 --- a/addons/sale_timesheet/wizard/sale_make_invoice_advance_views.xml +++ b/addons/sale_timesheet/wizard/sale_make_invoice_advance_views.xml @@ -1,5 +1,6 @@ + sale_timesheet.sale.advance.payment.inv.view.form sale.advance.payment.inv @@ -8,7 +9,7 @@ true - + + +