# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import SUPERUSER_ID, _, api, fields, models from odoo.exceptions import UserError from odoo.fields import Command from odoo.tools import formatLang class SaleAdvancePaymentInv(models.TransientModel): _name = 'sale.advance.payment.inv' _description = "Sales Advance Payment Invoice" 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')) # 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) # New Down Payment amount = fields.Float( string="Down Payment", help="The percentage of amount to be invoiced in advance.") fixed_amount = fields.Monetary( string="Down Payment Amount (Fixed)", help="The fixed amount to be invoiced in advance.") 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) amount_invoiced = fields.Monetary( string="Already invoiced", compute="_compute_invoice_amounts", help="Only confirmed down payments are considered.") # UI display_draft_invoice_warning = fields.Boolean(compute="_compute_display_draft_invoice_warning") consolidated_billing = fields.Boolean( string="Consolidated Billing", default=True, help="Create one invoice for all orders related to same customer, same invoicing address" " and same delivery address." ) #=== 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') ) # 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.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 @api.depends('sale_order_ids') def _compute_display_draft_invoice_warning(self): for wizard in self: invoice_states = wizard.sale_order_ids._origin.sudo().invoice_ids.mapped('state') wizard.display_draft_invoice_warning = 'draft' in invoice_states @api.depends('sale_order_ids') def _compute_invoice_amounts(self): for wizard in self: wizard.amount_invoiced = sum(wizard.sale_order_ids._origin.mapped('amount_invoiced')) #=== ONCHANGE METHODS ===# @api.onchange('advance_payment_method') def _onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': amount = self.default_get(['amount']).get('amount') return {'value': {'amount': amount}} #=== CONSTRAINT METHODS ===# 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.')) #=== ACTION METHODS ===# def create_invoices(self): self._check_amount_is_positive() invoices = self._create_invoices(self.sale_order_ids) return self.sale_order_ids.action_view_invoice(invoices=invoices) def view_draft_invoices(self): return { 'name': _('Draft Invoices'), 'type': 'ir.actions.act_window', 'view_mode': 'list', '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 ===# 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, grouped=not self.consolidated_billing) else: self.sale_order_ids.ensure_one() self = self.with_company(self.company_id) order = self.sale_order_ids AccountTax = self.env['account.tax'] order_lines = order.order_line.filtered(lambda x: not x.display_type) base_lines = [line._prepare_base_line_for_taxes_computation() for line in order_lines] AccountTax._add_tax_details_in_base_lines(base_lines, order.company_id) AccountTax._round_base_lines_tax_details(base_lines, order.company_id) if self.advance_payment_method == 'percentage': amount_type = 'percent' amount = self.amount else: # self.advance_payment_method == 'fixed': amount_type = 'fixed' amount = self.fixed_amount down_payment_base_lines = AccountTax._prepare_down_payment_lines( base_lines=base_lines, company=self.company_id, amount_type=amount_type, amount=amount, computation_key=f'down_payment,{self.id}', ) # Update the sale order. order._create_down_payment_section_line_if_needed() so_lines = order._create_down_payment_lines_from_base_lines(down_payment_base_lines) # Create the invoice. invoice_values = self.with_context(accounts=[ base_line['account_id'] or self._get_down_payment_account(base_line['product_id']) for base_line in down_payment_base_lines ])._prepare_down_payment_invoice_values( order=order, so_lines=so_lines, ) invoice_sudo = self.env['account.move'].sudo().create(invoice_values) # Unsudo the invoice after creation if not already sudoed invoice = invoice_sudo.sudo(self.env.su) poster = self.env.user._is_internal() and self.env.user.id or SUPERUSER_ID invoice.with_user(poster).message_post_with_source( 'mail.message_origin_link', render_values={'self': invoice, 'origin': order}, subtype_xmlid='mail.mt_note', ) title = _("Down payment invoice") order.with_user(poster).message_post( body=_("%s has been created", invoice._get_html_link(title=title)), ) return invoice # TODO: add accounts to method params in master def _prepare_down_payment_invoice_values(self, order, so_lines): """ Prepare the values to create a down payment invoice. :param order: The current sale order. :param so_lines: The "fake" down payment SO lines created on the sale order. :return: The values to create a new invoice. """ self.ensure_one() accounts = self.env.context.get('accounts') return { **order._prepare_invoice(), 'invoice_line_ids': [ Command.create(self._prepare_down_payment_invoice_line_values(order, so_line, self.company_id.downpayment_account_id or account)) for so_line, account in zip(so_lines, accounts) ], } def _prepare_down_payment_invoice_line_values(self, order, so_line, account): """ Prepare the invoice line values to be part of a down payment invoice. :param order: The current sale order. :param so_line: The "fake" down payment SO line created on the sale order. :param account: The down payment account to use. :return: The values to create a new invoice line. """ self.ensure_one() self = self.with_context(lang=order._get_lang()) if self.advance_payment_method == 'percentage': name = self.env._("Down payment of %s%%", formatLang(self.env, self.amount)) else: name = self.env._("Down Payment") return so_line._prepare_invoice_line( name=name, quantity=1.0, **({'account_id': account.id} if account else {}), ) def _get_down_payment_account(self, product): """ Retrieve the down payment account to use. :param product: A product. :return: An accounting account or None if not found. """ product_account = product.product_tmpl_id.get_product_accounts( fiscal_pos=self.sale_order_ids.fiscal_position_id ) return product_account.get('downpayment') or product_account.get('income')