[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) <vfe@odoo.com>
This commit is contained in:
Victor Feyens
2022-09-02 23:15:44 +02:00
parent a680441dda
commit aa9dcc2bce
10 changed files with 360 additions and 259 deletions
@@ -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)))))
@@ -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':
+1
View File
@@ -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',
+4 -6
View File
@@ -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')
+7 -6
View File
@@ -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
+25 -1
View File
@@ -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
+219 -165
View File
@@ -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
@@ -1,64 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_advance_payment_inv" model="ir.ui.view">
<field name="name">Invoice Orders</field>
<field name="model">sale.advance.payment.inv</field>
<field name="arch" type="xml">
<form string="Invoice Sales Order">
<p class="oe_grey">
Invoices will be created in draft so that you can review
them before validation.
</p>
<group>
<field name="count" attrs="{'invisible': [('count','=', 1)]}" readonly="True"/>
<field name="advance_payment_method" class="oe_inline" widget="radio"
attrs="{'invisible': [('count','&gt;',1)]}"/>
<field name="has_down_payments" invisible="1" />
<label for="deduct_down_payments" string="" attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
<div attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
id="down_payment_details">
<field name="deduct_down_payments" nolabel="1"/>
<label for="deduct_down_payments"/>
</div>
<field name="product_id"
context="{'default_invoice_policy': 'order'}" class="oe_inline"
invisible="1"/>
<label for="amount" attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"/>
<div attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"
id="payment_method_details">
<field name="currency_id" invisible="1"/>
<field name="fixed_amount"
attrs="{'required': [('advance_payment_method', '=', 'fixed')], 'invisible': [('advance_payment_method', '!=','fixed')]}" class="oe_inline"/>
<field name="amount"
attrs="{'required': [('advance_payment_method', '=', 'percentage')], 'invisible': [('advance_payment_method', '!=', 'percentage')]}" class="oe_inline"/>
<span
attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}" class="oe_inline">%</span>
</div>
<field name="deposit_account_id" options="{'no_create': True}" class="oe_inline"
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}" groups="account.group_account_manager"/>
<field name="deposit_taxes_id" class="oe_inline" widget="many2many_tags"
domain="[('type_tax_use','=','sale')]"
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
</group>
<footer>
<button name="create_invoices" id="create_invoice_open" string="Create and View Invoice" type="object"
context="{'open_invoices': True}" class="btn-primary" data-hotkey="q"/>
<button name="create_invoices" id="create_invoice" string="Create Invoice" type="object" data-hotkey="w"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_view_sale_advance_payment_inv" model="ir.actions.act_window">
<field name="name">Create invoices</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.advance.payment.inv</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<!-- TODO: check if we need this -->
<field name="binding_model_id" ref="sale.model_sale_order" />
<field name="binding_view_types">list</field>
</record>
<record id="view_sale_advance_payment_inv" model="ir.ui.view">
<field name="name">Invoice Orders</field>
<field name="model">sale.advance.payment.inv</field>
<field name="arch" type="xml">
<form string="Invoice Sales Order">
<p class="oe_grey">
Invoices will be created in draft so that you can review
them before validation.
</p>
<group>
<field name="sale_order_ids" invisible="1"/>
<field name="has_down_payments" invisible="1"/>
<field name="count" attrs="{'invisible': [('count', '=', 1)]}"/>
<field name="advance_payment_method" class="oe_inline"
widget="radio"
attrs="{'invisible': [('count', '&gt;', 1)]}"/>
<label for="deduct_down_payments"
string=""
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
<div attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
id="down_payment_details">
<field name="deduct_down_payments" nolabel="1"/>
<label for="deduct_down_payments"/>
</div>
</group>
<group name="down_payment_specification"
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed', 'percentage'))]}">
<field name="company_id" invisible="1"/>
<field name="product_id" invisible="1"/>
<label for="amount"/>
<div id="payment_method_details">
<field name="currency_id" invisible="1"/>
<field name="fixed_amount"
attrs="{'required': [('advance_payment_method', '=', 'fixed')],
'invisible': [('advance_payment_method', '!=', 'fixed')]}"
class="oe_inline"/>
<field name="amount"
attrs="{'required': [('advance_payment_method', '=', 'percentage')],
'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline"/>
<span attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline">%</span>
</div>
<field name="deposit_account_id"
options="{'no_create': True}"
attrs="{'invisible': [('product_id', '!=', False)]}"
groups="account.group_account_manager"/>
<field name="deposit_taxes_id"
widget="many2many_tags"
attrs="{'invisible': [('product_id', '!=', False)]}"/>
</group>
<footer>
<button name="create_invoices" type="object"
id="create_invoice_open"
string="Create and View Invoice"
context="{'open_invoices': True}"
class="btn-primary" data-hotkey="q"/>
<button name="create_invoices" type="object"
id="create_invoice"
string="Create Invoice"
data-hotkey="w"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_view_sale_advance_payment_inv" model="ir.actions.act_window">
<field name="name">Create invoices</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.advance.payment.inv</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_view_types">list</field>
</record>
</odoo>
@@ -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,
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="sale_advance_payment_inv_timesheet_view_form" model="ir.ui.view">
<field name="name">sale_timesheet.sale.advance.payment.inv.view.form</field>
<field name="model">sale.advance.payment.inv</field>
@@ -8,7 +9,7 @@
<xpath expr="//form" position="attributes">
<attribute name="disable_autofocus">true</attribute>
</xpath>
<xpath expr="//field[@name='deposit_taxes_id']" position="after">
<field name="deposit_taxes_id" position="after">
<field name="invoicing_timesheet_enabled" invisible="1"/>
<label for="date_start_invoice_timesheet" string="Timesheets Period" attrs="{'invisible': [ '|', ('invoicing_timesheet_enabled', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
<div class="o_row" attrs="{'invisible': [ '|',('invoicing_timesheet_enabled', '=', False), ('advance_payment_method', '!=', 'delivered')]}">
@@ -22,7 +23,8 @@
options="{'related_start_date': 'date_start_invoice_timesheet'}"
title="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."/>
</div>
</xpath>
</field>
</field>
</record>
</odoo>