[IMP] sale: Only record down payments when confirmed
Until now, there was no way on a given sales order to distinguish draft, cancelled and posted down payments. This commit makes sure the down payments state is clearly shown as draft or cancelled when the linked invoice is in the corresponding states. Also remove those draft and cancelled down payments from the sale portal & report as it can be confusing for customers to see line(s) of down payments they never heard of. Lastly, follow the existing behavior of invoices, where the down payments were shown in a separate section. Now a dedicated section will be added to the sales order and propagated to the invoice, if there are down payments to deduct. Task - 2702407 closes odoo/odoo#82376 Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
committed by
Victor Feyens
parent
dc53124a09
commit
d5b4534bf9
@@ -61,8 +61,12 @@ class AccountMove(models.Model):
|
||||
def action_post(self):
|
||||
#inherit of the function from account.move to validate a new tax and the priceunit of a downpayment
|
||||
res = super(AccountMove, self).action_post()
|
||||
line_ids = self.mapped('line_ids').filtered(lambda line: line.sale_line_ids.is_downpayment)
|
||||
for line in line_ids:
|
||||
down_payment_lines = self.line_ids.filtered(lambda line: line.sale_line_ids.is_downpayment)
|
||||
for line in down_payment_lines:
|
||||
|
||||
if not line.sale_line_ids.display_type:
|
||||
line.sale_line_ids._compute_name()
|
||||
|
||||
try:
|
||||
line.sale_line_ids.tax_id = line.tax_ids
|
||||
if all(line.tax_ids.mapped('price_include')):
|
||||
@@ -77,6 +81,24 @@ class AccountMove(models.Model):
|
||||
pass
|
||||
return res
|
||||
|
||||
def button_draft(self):
|
||||
res = super().button_draft()
|
||||
|
||||
self.line_ids.filtered(
|
||||
lambda line: line.sale_line_ids.is_downpayment and not line.sale_line_ids.display_type
|
||||
).sale_line_ids._compute_name()
|
||||
|
||||
return res
|
||||
|
||||
def button_cancel(self):
|
||||
res = super().button_cancel()
|
||||
|
||||
self.line_ids.filtered(
|
||||
lambda line: line.sale_line_ids.is_downpayment and not line.sale_line_ids.display_type
|
||||
).sale_line_ids._compute_name()
|
||||
|
||||
return res
|
||||
|
||||
def _post(self, soft=True):
|
||||
# OVERRIDE
|
||||
# Auto-reconcile the invoice with payments coming from transactions.
|
||||
|
||||
@@ -1245,6 +1245,25 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
return self.transaction_ids._get_last()
|
||||
|
||||
def _get_order_lines_to_report(self):
|
||||
down_payment_lines = self.order_line.filtered(lambda line:
|
||||
line.is_downpayment
|
||||
and not line.display_type
|
||||
and not line._get_downpayment_state()
|
||||
)
|
||||
|
||||
def show_line(line):
|
||||
if not line.is_downpayment:
|
||||
return True
|
||||
elif line.display_type and down_payment_lines:
|
||||
return True # Only show the down payment section if down payments were posted
|
||||
elif line in down_payment_lines:
|
||||
return True # Only show posted down payments
|
||||
else:
|
||||
return False
|
||||
|
||||
return self.order_line.filtered(show_line)
|
||||
|
||||
# PORTAL #
|
||||
|
||||
def has_to_be_signed(self, include_draft=False):
|
||||
|
||||
@@ -291,7 +291,17 @@ class SaleOrderLine(models.Model):
|
||||
for line in self:
|
||||
if not line.product_id:
|
||||
continue
|
||||
line.name = line.with_context(lang=line.order_partner_id.lang)._get_sale_order_line_multiline_description_sale()
|
||||
|
||||
name = line.with_context(lang=line.order_partner_id.lang)._get_sale_order_line_multiline_description_sale()
|
||||
if line.is_downpayment and not line.display_type:
|
||||
context = {'lang': line.order_partner_id.lang}
|
||||
dp_state = line._get_downpayment_state()
|
||||
if dp_state == 'draft':
|
||||
name = _("%(line_description)s (Draft)", line_description=name)
|
||||
elif dp_state == 'cancel':
|
||||
name = _("%(line_description)s (Canceled)", line_description=name)
|
||||
del context
|
||||
line.name = name
|
||||
|
||||
def _get_sale_order_line_multiline_description_sale(self):
|
||||
""" Compute a default multiline description for this sales order line.
|
||||
@@ -654,6 +664,20 @@ class SaleOrderLine(models.Model):
|
||||
for so_line in lines_by_analytic:
|
||||
so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0)
|
||||
|
||||
def _get_downpayment_state(self):
|
||||
self.ensure_one()
|
||||
|
||||
if self.display_type:
|
||||
return ''
|
||||
|
||||
invoice_lines = self._get_invoice_lines()
|
||||
if all(line.parent_state == 'draft' for line in invoice_lines):
|
||||
return 'draft'
|
||||
if all(line.parent_state == 'cancel' for line in invoice_lines):
|
||||
return 'cancel'
|
||||
|
||||
return ''
|
||||
|
||||
def _get_delivered_quantity_by_analytic(self, additional_domain):
|
||||
""" Compute and write the delivered quantity of current SO lines, based on their related
|
||||
analytic lines.
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Is there a discount on at least one line? -->
|
||||
<t t-set="display_discount" t-value="any(l.discount for l in doc.order_line)"/>
|
||||
<t t-set="lines_to_report" t-value="doc._get_order_lines_to_report()"/>
|
||||
<t t-set="display_discount" t-value="any(l.discount for l in lines_to_report)"/>
|
||||
|
||||
<table class="table table-sm o_main_table mt-4">
|
||||
<!-- In case we want to repeat the header, remove "display: table-row-group" -->
|
||||
@@ -86,7 +87,7 @@
|
||||
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<t t-foreach="lines_to_report" t-as="line">
|
||||
|
||||
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
@@ -107,7 +108,7 @@
|
||||
<td name="td_taxes" class="text-end">
|
||||
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
|
||||
</td>
|
||||
<td name="td_subtotal" class="text-end o_price_total">
|
||||
<td t-if="not line.is_downpayment" name="td_subtotal" class="text-end o_price_total">
|
||||
<span t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
</td>
|
||||
@@ -126,7 +127,7 @@
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-if="current_section and (line_last or doc.order_line[line_index+1].display_type == 'line_section')">
|
||||
<t t-if="current_section and (line_last or doc.order_line[line_index+1].display_type == 'line_section') and not line.is_downpayment">
|
||||
<tr class="is-subtotal text-end">
|
||||
<td name="td_section_subtotal" colspan="99">
|
||||
<strong class="mr16">Subtotal</strong>
|
||||
|
||||
@@ -344,7 +344,8 @@ class TestSaleRefund(TestSaleCommon):
|
||||
})
|
||||
downpayment.create_invoices()
|
||||
sale_order_refund.invoice_ids[0].action_post()
|
||||
sol_downpayment = sale_order_refund.order_line[1]
|
||||
# order_line[1] is the down payment section
|
||||
sol_downpayment = sale_order_refund.order_line[2]
|
||||
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({
|
||||
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||||
@@ -352,7 +353,8 @@ class TestSaleRefund(TestSaleCommon):
|
||||
payment.create_invoices()
|
||||
|
||||
so_invoice = max(sale_order_refund.invoice_ids)
|
||||
self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), len(sale_order_refund.order_line), 'All lines should be invoiced')
|
||||
self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))),
|
||||
len(sale_order_refund.order_line.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), 'All lines should be invoiced')
|
||||
self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present')
|
||||
self.assertEqual(so_invoice.amount_total, sale_order_refund.amount_total - sol_downpayment.price_unit, 'Downpayment should be applied')
|
||||
so_invoice.action_post()
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||||
|
||||
self.assertEqual(len(self.sale_order.invoice_ids), 2, 'Invoice should be created for the SO')
|
||||
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment)
|
||||
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type)
|
||||
self.assertEqual(len(downpayment_line), 2, 'SO line downpayment should be created on SO')
|
||||
|
||||
# Update delivered quantity of SO lines
|
||||
@@ -124,7 +124,8 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
self.assertEqual(len(self.sale_order.invoice_ids), 3, 'Invoice should be created for the SO')
|
||||
|
||||
invoice = max(self.sale_order.invoice_ids)
|
||||
self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), len(self.sale_order.order_line), 'All lines should be invoiced')
|
||||
self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))),
|
||||
len(self.sale_order.order_line.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), 'All lines should be invoiced')
|
||||
self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present')
|
||||
self.assertEqual(invoice.amount_total, self.sale_order.amount_total - sum(downpayment_line.mapped('price_unit')), 'Downpayment should be applied')
|
||||
|
||||
@@ -147,7 +148,7 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
payment.create_invoices()
|
||||
|
||||
self.assertEqual(len(self.sale_order.invoice_ids), 1, 'Invoice should be created for the SO')
|
||||
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment)
|
||||
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type)
|
||||
self.assertEqual(len(downpayment_line), 1, 'SO line downpayment should be created on SO')
|
||||
self.assertEqual(downpayment_line.price_unit, self.sale_order.amount_total/2, 'downpayment should have the correct amount')
|
||||
|
||||
|
||||
@@ -503,8 +503,9 @@
|
||||
optional="show"
|
||||
/>
|
||||
<field name="discount" string="Disc.%" groups="product.group_discount_per_so_line" optional="show" widget="product_discount"/>
|
||||
<field name="price_subtotal" widget="monetary" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<field name="price_total" widget="monetary" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<field name="is_downpayment" invisible="1"/>
|
||||
<field name="price_subtotal" widget="monetary" groups="account.group_show_line_subtotals_tax_excluded" attrs="{'invisible': [('is_downpayment', '=', True)]}"/>
|
||||
<field name="price_total" widget="monetary" groups="account.group_show_line_subtotals_tax_included" attrs="{'invisible': [('is_downpayment', '=', True)]}"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
|
||||
@@ -483,8 +483,9 @@
|
||||
<tbody class="sale_tbody">
|
||||
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
<t t-set="lines_to_report" t-value="sale_order._get_order_lines_to_report()"/>
|
||||
|
||||
<t t-foreach="sale_order.order_line" t-as="line">
|
||||
<t t-foreach="lines_to_report" t-as="line">
|
||||
|
||||
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
@@ -519,7 +520,7 @@
|
||||
<td t-attf-class="text-end {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">
|
||||
<span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<td t-if="not line.is_downpayment" class="text-end">
|
||||
<span class="oe_order_line_price_subtotal" t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<span class="oe_order_line_price_total" t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
</td>
|
||||
@@ -537,8 +538,7 @@
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<tr t-if="current_section and (line_last or sale_order.order_line[line_index+1].display_type == 'line_section')"
|
||||
<tr t-if="current_section and (line_last or lines_to_report[line_index+1].display_type == 'line_section') and not line.is_downpayment"
|
||||
class="is-subtotal text-end">
|
||||
<td colspan="99">
|
||||
<strong class="mr16">Subtotal</strong>
|
||||
|
||||
@@ -136,10 +136,25 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return invoice
|
||||
|
||||
def _prepare_downpayment_section(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(self, order, analytic_tag_ids, tax_ids, amount):
|
||||
context = {'lang': order.partner_id.lang}
|
||||
so_values = {
|
||||
'name': _('Down Payment: %s') % (time.strftime('%m %Y'),),
|
||||
'name': _('Down Payment: %s (Draft)') % (time.strftime('%m %Y'),),
|
||||
'price_unit': amount,
|
||||
'product_uom_qty': 0.0,
|
||||
'order_id': order.id,
|
||||
@@ -179,6 +194,11 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user