# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import time from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.fields import Command from odoo.tools import float_is_zero 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, 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) # 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") #=== 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') # '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): if self.advance_payment_method == 'percentage': amount = self.default_get(['amount']).get('amount') return {'value': {'amount': amount}} #=== 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)], } 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_so_line_values(self, order): self.ensure_one() analytic_distribution = {} amount_total = sum(order.order_line.mapped("price_total")) if not float_is_zero(amount_total, precision_rounding=self.currency_id.rounding): for line in order.order_line: distrib_dict = line.analytic_distribution or {} for account, distribution in distrib_dict.items(): analytic_distribution[account] = distribution * line.price_total + analytic_distribution.get(account, 0) for account, distribution_amount in analytic_distribution.items(): analytic_distribution[account] = distribution_amount/amount_total context = {'lang': order.partner_id.lang} so_values = { '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_id': self.product_id.id, 'analytic_distribution': analytic_distribution, 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values def _get_down_payment_amount(self, order): self.ensure_one() if self.advance_payment_method == 'percentage': advance_product_taxes = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == order.company_id) if all(order.fiscal_position_id.map_tax(advance_product_taxes).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 def _prepare_invoice_values(self, order, so_line): self.ensure_one() return { **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