# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models, SUPERUSER_ID from odoo.exceptions import UserError from odoo.fields import Command from odoo.tools import format_date, frozendict 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 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.") 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.") 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 deposit_account_id = fields.Many2one( comodel_name='account.account', string="Income Account", domain=[('deprecated', '=', False)], check_company=True, help="Account used for deposits") deposit_taxes_id = fields.Many2many( comodel_name='account.tax', string="Customer Taxes", domain=[('type_tax_use', '=', 'sale')], check_company=True, help="Taxes used for deposits") # 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") consolidated_billing = fields.Boolean( string="Consolidated Billing", default=True, help="Create one invoice for all orders related to same customer and same invoicing 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('company_id') def _compute_product_id(self): self.product_id = False for wizard in self: if wizard.count == 1: wizard.product_id = wizard.company_id.sale_down_payment_product_id @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.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._origin.mapped('amount_invoiced')) wizard.amount_to_invoice = sum(wizard.sale_order_ids._origin.mapped('amount_to_invoice')) #=== 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.')) @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._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': '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 ===# 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 # Create deposit product if necessary if not self.product_id: self.company_id.sale_down_payment_product_id = self.env['product.product'].create( self._prepare_down_payment_product_values() ) self._compute_product_id() # Create down payment section if necessary SaleOrderline = self.env['sale.order.line'].with_context(sale_no_log_for_new_lines=True) if not any(line.display_type and line.is_downpayment for line in order.order_line): SaleOrderline.create( self._prepare_down_payment_section_values(order) ) down_payment_lines = SaleOrderline.create( self._prepare_down_payment_lines_values(order) ) invoice = self.env['account.move'].sudo().create( self._prepare_invoice_values(order, down_payment_lines) ).with_user(self.env.uid) # Unsudo the invoice after creation # Ensure the invoice total is exactly the expected fixed amount. if self.advance_payment_method == 'fixed': delta_amount = (invoice.amount_total - self.fixed_amount) * (1 if invoice.is_inbound() else -1) if not order.currency_id.is_zero(delta_amount): receivable_line = invoice.line_ids\ .filtered(lambda aml: aml.account_id.account_type == 'asset_receivable')[:1] product_lines = invoice.line_ids\ .filtered(lambda aml: aml.display_type == 'product') tax_lines = invoice.line_ids\ .filtered(lambda aml: aml.tax_line_id.amount_type not in (False, 'fixed')) if product_lines and tax_lines and receivable_line: line_commands = [Command.update(receivable_line.id, { 'amount_currency': receivable_line.amount_currency + delta_amount, })] delta_sign = 1 if delta_amount > 0 else -1 for lines, attr, sign in ( (product_lines, 'price_total', -1), (tax_lines, 'amount_currency', 1), ): remaining = delta_amount lines_len = len(lines) for line in lines: if order.currency_id.compare_amounts(remaining, 0) != delta_sign: break amt = delta_sign * max( order.currency_id.rounding, abs(order.currency_id.round(remaining / lines_len)), ) remaining -= amt line_commands.append(Command.update(line.id, {attr: line[attr] + amt * sign})) invoice.line_ids = line_commands 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 def _prepare_down_payment_product_values(self): self.ensure_one() return { 'name': _('Down payment'), 'type': 'service', 'invoice_policy': 'order', 'company_id': self.company_id.id, 'property_account_income_id': self.deposit_account_id.id, 'taxes_id': [Command.set(self.deposit_taxes_id.ids)], } def _prepare_down_payment_section_values(self, order): context = {'lang': order.partner_id.lang} so_values = { 'name': _('Down Payments'), 'product_uom_qty': 0.0, 'order_id': order.id, 'display_type': 'line_section', 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values def _prepare_down_payment_lines_values(self, order): """ Create one down payment line per tax or unique taxes combination. Apply the tax(es) to their respective lines. :param order: Order for which the down payment lines are created. :return: An array of dicts with the down payment lines values. """ self.ensure_one() if self.advance_payment_method == 'percentage': percentage = self.amount / 100 else: percentage = self.fixed_amount / order.amount_total if order.amount_total else 1 order_lines = order.order_line.filtered(lambda l: not l.display_type) base_downpayment_lines_values = self._prepare_base_downpayment_line_values(order) tax_base_line_dicts = [ line._convert_to_tax_base_line_dict( analytic_distribution=line.analytic_distribution, handle_price_include=False ) for line in order_lines ] computed_taxes = self.env['account.tax']._compute_taxes( tax_base_line_dicts) down_payment_values = [] for line, tax_repartition in computed_taxes['base_lines_to_update']: taxes = line['taxes'].flatten_taxes_hierarchy() fixed_taxes = taxes.filtered(lambda tax: tax.amount_type == 'fixed') down_payment_values.append([ taxes - fixed_taxes, line['analytic_distribution'], tax_repartition['price_subtotal'] ]) for fixed_tax in fixed_taxes: # Fixed taxes cannot be set as taxes on down payments as they always amounts to 100% # of the tax amount. Therefore fixed taxes are removed and are replace by a new line # with appropriate amount, and non fixed taxes if the fixed tax affected the base of # any other non fixed tax. if fixed_tax.include_base_amount: pct_tax = taxes[list(taxes).index(fixed_tax) + 1:]\ .filtered(lambda t: t.is_base_affected and t.amount_type != 'fixed') else: pct_tax = self.env['account.tax'] down_payment_values.append([ pct_tax, line['analytic_distribution'], line['quantity'] * fixed_tax.amount ]) downpayment_line_map = {} for tax_id, analytic_distribution, price_subtotal in down_payment_values: grouping_key = frozendict({ 'tax_id': tuple(sorted(tax_id.ids)), 'analytic_distribution': analytic_distribution, }) downpayment_line_map.setdefault(grouping_key, { **base_downpayment_lines_values, **grouping_key, 'product_uom_qty': 0.0, 'price_unit': 0.0, }) downpayment_line_map[grouping_key]['price_unit'] += \ order.currency_id.round(price_subtotal * percentage) return list(downpayment_line_map.values()) def _prepare_base_downpayment_line_values(self, order): self.ensure_one() context = {'lang': order.partner_id.lang} so_values = { 'name': _( 'Down Payment: %(date)s (Draft)', date=format_date(self.env, fields.Date.today()) ), 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, 'product_id': self.product_id.id, 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values def _prepare_invoice_values(self, order, so_lines): self.ensure_one() return { **order._prepare_invoice(), 'invoice_line_ids': [Command.create( line._prepare_invoice_line( name=self._get_down_payment_description(order), quantity=1.0, ) ) for line in so_lines], } 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