1074 lines
53 KiB
Python
1074 lines
53 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import fields
|
||
|
from odoo.fields import Command
|
||
|
from odoo.tests import Form, tagged
|
||
|
from odoo.tools import float_is_zero
|
||
|
|
||
|
from odoo.addons.sale.tests.common import TestSaleCommon
|
||
|
|
||
|
|
||
|
@tagged('-at_install', 'post_install')
|
||
|
class TestSaleToInvoice(TestSaleCommon):
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls, chart_template_ref=None):
|
||
|
super().setUpClass(chart_template_ref=chart_template_ref)
|
||
|
|
||
|
# Create the SO with four order lines
|
||
|
cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
|
||
|
'partner_id': cls.partner_a.id,
|
||
|
'partner_invoice_id': cls.partner_a.id,
|
||
|
'partner_shipping_id': cls.partner_a.id,
|
||
|
'pricelist_id': cls.company_data['default_pricelist'].id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': cls.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 5,
|
||
|
'tax_id': False,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': cls.company_data['product_service_delivery'].id,
|
||
|
'product_uom_qty': 4,
|
||
|
'tax_id': False,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': cls.company_data['product_service_order'].id,
|
||
|
'product_uom_qty': 3,
|
||
|
'tax_id': False,
|
||
|
}),
|
||
|
Command.create({
|
||
|
'product_id': cls.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 2,
|
||
|
'tax_id': False,
|
||
|
}),
|
||
|
]
|
||
|
})
|
||
|
|
||
|
(
|
||
|
cls.sol_prod_order,
|
||
|
cls.sol_serv_deliver,
|
||
|
cls.sol_serv_order,
|
||
|
cls.sol_prod_deliver,
|
||
|
) = cls.sale_order.order_line
|
||
|
|
||
|
# Context
|
||
|
cls.context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [cls.sale_order.id],
|
||
|
'active_id': cls.sale_order.id,
|
||
|
'default_journal_id': cls.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
|
||
|
def _check_order_search(self, orders, domain, expected_result):
|
||
|
domain += [('id', 'in', orders.ids)]
|
||
|
result = self.env['sale.order'].search(domain)
|
||
|
self.assertEqual(result, expected_result, "Unexpected result on search orders")
|
||
|
|
||
|
def test_search_invoice_ids(self):
|
||
|
"""Test searching on computed fields invoice_ids"""
|
||
|
|
||
|
# Make qty zero to have a line without invoices
|
||
|
self.sol_prod_order.product_uom_qty = 0
|
||
|
self.sale_order.action_confirm()
|
||
|
|
||
|
# Tests before creating an invoice
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '!=', False)], self.env['sale.order'])
|
||
|
|
||
|
# Create invoice
|
||
|
moves = self.sale_order._create_invoices()
|
||
|
|
||
|
# Tests after creating the invoice
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', 'in', moves.ids)], self.sale_order)
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '!=', False)], self.sale_order)
|
||
|
|
||
|
def test_downpayment(self):
|
||
|
""" Test invoice with a way of downpayment and check downpayment's SO line is created
|
||
|
and also check a total amount of invoice is equal to a respective sale order's total amount
|
||
|
"""
|
||
|
# Confirm the SO
|
||
|
self.sale_order.action_confirm()
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||
|
# Let's do an invoice for a deposit of 100
|
||
|
downpayment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
downpayment.create_invoices()
|
||
|
downpayment2 = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
downpayment2.create_invoices()
|
||
|
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 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
|
||
|
self.sol_serv_deliver.write({'qty_delivered': 4.0})
|
||
|
self.sol_prod_deliver.write({'qty_delivered': 2.0})
|
||
|
|
||
|
# Let's do an invoice with refunds
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
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.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')
|
||
|
|
||
|
def test_downpayment_validation(self):
|
||
|
""" Test invoice for downpayment and check it can be validated
|
||
|
"""
|
||
|
# Lock the sale orders when confirmed
|
||
|
self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
|
||
|
|
||
|
# Confirm the SO
|
||
|
self.sale_order.action_confirm()
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||
|
# Let's do an invoice for a deposit of 10%
|
||
|
downpayment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'percentage',
|
||
|
'amount': 10,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
downpayment.create_invoices()
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||
|
|
||
|
# Update delivered quantity of SO lines
|
||
|
self.sol_serv_deliver.write({'qty_delivered': 4.0})
|
||
|
self.sol_prod_deliver.write({'qty_delivered': 2.0})
|
||
|
|
||
|
# Validate invoice
|
||
|
self.sale_order.invoice_ids.action_post()
|
||
|
|
||
|
def test_downpayment_line_remains_on_SO(self):
|
||
|
""" Test downpayment's SO line is created and remains unchanged even if everything is invoiced
|
||
|
"""
|
||
|
# Create the SO with one line
|
||
|
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'partner_shipping_id': self.partner_a.id,
|
||
|
'pricelist_id': self.company_data['default_pricelist'].id,
|
||
|
'order_line': [Command.create({
|
||
|
'product_id': self.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 5,
|
||
|
'tax_id': False,
|
||
|
}),]
|
||
|
})
|
||
|
# Confirm the SO
|
||
|
sale_order.action_confirm()
|
||
|
# Update delivered quantity of SO line
|
||
|
sale_order.order_line.write({'qty_delivered': 5.0})
|
||
|
context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [sale_order.id],
|
||
|
'active_id': sale_order.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
# Let's do an invoice for a down payment of 50
|
||
|
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
downpayment.create_invoices()
|
||
|
# Let's do the invoice for the remaining amount
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
downpayment_line = sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type)
|
||
|
self.assertEqual(downpayment_line[0].price_unit, 50, 'The down payment unit price should not change on SO')
|
||
|
# Confirm all invoices
|
||
|
sale_order.invoice_ids.action_post()
|
||
|
self.assertEqual(downpayment_line[0].price_unit, 50, 'The down payment unit price should not change on SO')
|
||
|
|
||
|
def test_downpayment_fixed_amount_with_zero_total_amount(self):
|
||
|
# Create the SO with one line and amount total is zero
|
||
|
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'partner_shipping_id': self.partner_a.id,
|
||
|
'pricelist_id': self.company_data['default_pricelist'].id,
|
||
|
'order_line': [Command.create({
|
||
|
'product_id': self.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 5,
|
||
|
'price_unit': 0,
|
||
|
'tax_id': False,
|
||
|
}), ]
|
||
|
})
|
||
|
sale_order.action_confirm()
|
||
|
sale_order.order_line.write({'qty_delivered': 5.0})
|
||
|
context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [sale_order.id],
|
||
|
'active_id': sale_order.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
# Let's do an invoice for a down payment of 50
|
||
|
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
# Create invoice
|
||
|
downpayment.create_invoices()
|
||
|
self.assertEqual(downpayment.amount, 0.0, 'The down payment amount should be 0.0')
|
||
|
|
||
|
def test_downpayment_percentage_tax_icl(self):
|
||
|
""" Test invoice with a percentage downpayment and an included tax
|
||
|
Check the total amount of invoice is correct and equal to a respective sale order's total amount
|
||
|
"""
|
||
|
# Confirm the SO
|
||
|
self.sale_order.action_confirm()
|
||
|
tax_downpayment = self.company_data['default_tax_sale'].copy({
|
||
|
'name': 'default price included',
|
||
|
'price_include': True,
|
||
|
})
|
||
|
# Let's do an invoice for a deposit of 100
|
||
|
product_id = self.env.company.sale_down_payment_product_id
|
||
|
product_id.taxes_id = tax_downpayment.ids
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'percentage',
|
||
|
'amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id,
|
||
|
})
|
||
|
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 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')
|
||
|
|
||
|
invoice = self.sale_order.invoice_ids[0]
|
||
|
downpayment_aml = invoice.line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))[0]
|
||
|
self.assertEqual(downpayment_aml.price_total, self.sale_order.amount_total/2, 'downpayment should have the correct amount')
|
||
|
self.assertEqual(downpayment_aml.price_unit, self.sale_order.amount_total/2, 'downpayment should have the correct amount')
|
||
|
invoice.action_post()
|
||
|
self.assertEqual(downpayment_line.price_unit, self.sale_order.amount_total/2, 'downpayment should have the correct amount')
|
||
|
|
||
|
def test_downpayment_invoice_and_partial_credit_note(self):
|
||
|
"""This test check that the downpayment line amount on the sale order remains consistent"""
|
||
|
self.sale_order.action_confirm()
|
||
|
|
||
|
# Create an invoice for a Down payment of 100
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 100,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id,
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
# Ensure the downpayment line on the sale order is correctly set to 100
|
||
|
downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type)
|
||
|
self.assertEqual(downpayment_line.price_unit, 100)
|
||
|
|
||
|
# post the downpayment invoice and ensure the downpayment_line amount is still 100
|
||
|
downpayment_invoice = downpayment_line.order_id.order_line.invoice_lines.move_id
|
||
|
downpayment_invoice.action_post()
|
||
|
self.assertEqual(downpayment_line.price_unit, 100)
|
||
|
|
||
|
# Create a credit note for a part of the downpayment invoice and post it
|
||
|
move_reversal = self.env['account.move.reversal'].with_context(
|
||
|
active_model="account.move",
|
||
|
active_ids=downpayment_invoice.ids,
|
||
|
).create({
|
||
|
'date': '2020-02-01',
|
||
|
'reason': 'no reason',
|
||
|
'journal_id': downpayment_invoice.journal_id.id,
|
||
|
})
|
||
|
reversal_action = move_reversal.reverse_moves()
|
||
|
reverse_move = self.env['account.move'].browse(reversal_action['res_id'])
|
||
|
with Form(reverse_move) as form_reverse:
|
||
|
with form_reverse.invoice_line_ids.edit(0) as line_form:
|
||
|
line_form.price_unit = 20.0
|
||
|
reverse_move.action_post()
|
||
|
|
||
|
self.assertEqual(downpayment_line.price_unit, 80,
|
||
|
"The downpayment line amount should be equal to the sum of the invoice and credit note amount")
|
||
|
|
||
|
def test_invoice_with_discount(self):
|
||
|
""" Test invoice with a discount and check discount applied on both SO lines and an invoice lines """
|
||
|
# Update discount and delivered quantity on SO lines
|
||
|
self.sol_prod_order.write({'discount': 20.0})
|
||
|
self.sol_serv_deliver.write({'discount': 20.0, 'qty_delivered': 4.0})
|
||
|
self.sol_serv_order.write({'discount': -10.0})
|
||
|
self.sol_prod_deliver.write({'qty_delivered': 2.0})
|
||
|
|
||
|
for line in self.sale_order.order_line.filtered(lambda l: l.discount):
|
||
|
product_price = line.price_unit * line.product_uom_qty
|
||
|
self.assertEqual(line.discount, (product_price - line.price_subtotal) / product_price * 100, 'Discount should be applied on order line')
|
||
|
|
||
|
# lines are in draft
|
||
|
for line in self.sale_order.order_line:
|
||
|
self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state")
|
||
|
self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")
|
||
|
|
||
|
self.sale_order.action_confirm()
|
||
|
|
||
|
for line in self.sale_order.order_line:
|
||
|
self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")
|
||
|
|
||
|
self.assertEqual(
|
||
|
self.sol_serv_order.untaxed_amount_to_invoice,
|
||
|
297,
|
||
|
"The untaxed amount to invoice is wrong")
|
||
|
self.assertEqual(
|
||
|
self.sol_serv_deliver.untaxed_amount_to_invoice,
|
||
|
576,
|
||
|
"The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)")
|
||
|
# 'untaxed_amount_to_invoice' is invalid when 'sale_stock' is installed.
|
||
|
# self.assertEqual(self.sol_prod_deliver.untaxed_amount_to_invoice, 140, "The untaxed amount to invoice should be qty deli * price reduce, so 4 * (180 - 36)")
|
||
|
|
||
|
# Let's do an invoice with invoiceable lines
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'delivered'
|
||
|
})
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.sale_order)
|
||
|
payment.create_invoices()
|
||
|
self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order'])
|
||
|
invoice = self.sale_order.invoice_ids[0]
|
||
|
invoice.action_post()
|
||
|
|
||
|
# Check discount appeared on both SO lines and invoice lines
|
||
|
for line, inv_line in zip(self.sale_order.order_line, invoice.invoice_line_ids):
|
||
|
self.assertEqual(line.discount, inv_line.discount, 'Discount on lines of order and invoice should be same')
|
||
|
|
||
|
def test_invoice(self):
|
||
|
""" Test create and invoice from the SO, and check qty invoice/to invoice, and the related amounts """
|
||
|
# lines are in draft
|
||
|
for line in self.sale_order.order_line:
|
||
|
self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state")
|
||
|
self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")
|
||
|
|
||
|
# Confirm the SO
|
||
|
self.sale_order.action_confirm()
|
||
|
|
||
|
# Check ordered quantity, quantity to invoice and invoiced quantity of SO lines
|
||
|
for line in self.sale_order.order_line:
|
||
|
if line.product_id.invoice_policy == 'delivery':
|
||
|
self.assertEqual(line.qty_to_invoice, 0.0, 'Quantity to invoice should be same as ordered quantity')
|
||
|
self.assertEqual(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||
|
else:
|
||
|
self.assertEqual(line.qty_to_invoice, line.product_uom_qty, 'Quantity to invoice should be same as ordered quantity')
|
||
|
self.assertEqual(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line is confirmed")
|
||
|
|
||
|
# Let's do an invoice with invoiceable lines
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'delivered'
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
invoice = self.sale_order.invoice_ids[0]
|
||
|
|
||
|
# Update quantity of an invoice lines
|
||
|
move_form = Form(invoice)
|
||
|
with move_form.invoice_line_ids.edit(0) as line_form:
|
||
|
line_form.quantity = 3.0
|
||
|
with move_form.invoice_line_ids.edit(1) as line_form:
|
||
|
line_form.quantity = 2.0
|
||
|
invoice = move_form.save()
|
||
|
|
||
|
# amount to invoice / invoiced should not have changed (amounts take only confirmed invoice into account)
|
||
|
for line in self.sale_order.order_line:
|
||
|
if line.product_id.invoice_policy == 'delivery':
|
||
|
self.assertEqual(line.qty_to_invoice, 0.0, "Quantity to invoice should be zero")
|
||
|
self.assertEqual(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as delivered lines are not delivered yet")
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity (no confirmed invoice)")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now")
|
||
|
else:
|
||
|
if line == self.sol_prod_order:
|
||
|
self.assertEqual(self.sol_prod_order.qty_to_invoice, 2.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines")
|
||
|
self.assertEqual(self.sol_prod_order.qty_invoiced, 3.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines")
|
||
|
else:
|
||
|
self.assertEqual(self.sol_serv_order.qty_to_invoice, 1.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines")
|
||
|
self.assertEqual(self.sol_serv_order.qty_invoiced, 2.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines")
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed (no confirmed invoice)")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now")
|
||
|
|
||
|
invoice.action_post()
|
||
|
|
||
|
# Check quantity to invoice on SO lines
|
||
|
for line in self.sale_order.order_line:
|
||
|
if line.product_id.invoice_policy == 'delivery':
|
||
|
self.assertEqual(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
|
||
|
self.assertEqual(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||
|
else:
|
||
|
if line == self.sol_prod_order:
|
||
|
self.assertEqual(line.qty_to_invoice, 2.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||
|
self.assertEqual(line.qty_invoiced, 3.0, "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)")
|
||
|
else:
|
||
|
self.assertEqual(line.qty_to_invoice, 1.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||
|
self.assertEqual(line.qty_invoiced, 2.0, "The ordered (serv) sale line are totally invoiced (qty invoiced = the invoice lines)")
|
||
|
self.assertEqual(line.untaxed_amount_to_invoice, line.price_unit * line.qty_to_invoice, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products")
|
||
|
self.assertEqual(line.untaxed_amount_invoiced, line.price_unit * line.qty_invoiced, "Amount invoiced is now set as qty invoiced * unit price since no price change on invoice, for ordered products")
|
||
|
|
||
|
def test_multiple_sale_orders_on_same_invoice(self):
|
||
|
""" The model allows the association of multiple SO lines linked to the same invoice line.
|
||
|
Check that the operations behave well, if a custom module creates such a situation.
|
||
|
"""
|
||
|
self.sale_order.action_confirm()
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'delivered'
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
# create a second SO whose lines are linked to the same invoice lines
|
||
|
# this is a way to create a situation where sale_line_ids has multiple items
|
||
|
sale_order_data = self.sale_order.copy_data()[0]
|
||
|
sale_order_data['order_line'] = [
|
||
|
(0, 0, line.copy_data({
|
||
|
'invoice_lines': [(6, 0, line.invoice_lines.ids)],
|
||
|
})[0])
|
||
|
for line in self.sale_order.order_line
|
||
|
]
|
||
|
self.sale_order.create(sale_order_data)
|
||
|
|
||
|
# we should now have at least one move line linked to several order lines
|
||
|
invoice = self.sale_order.invoice_ids[0]
|
||
|
self.assertTrue(any(len(move_line.sale_line_ids) > 1
|
||
|
for move_line in invoice.line_ids))
|
||
|
|
||
|
# however these actions should not raise
|
||
|
invoice.action_post()
|
||
|
invoice.button_draft()
|
||
|
invoice.button_cancel()
|
||
|
|
||
|
def test_invoice_with_sections(self):
|
||
|
""" Test create and invoice with sections from the SO, and check qty invoice/to invoice, and the related amounts """
|
||
|
|
||
|
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'partner_shipping_id': self.partner_a.id,
|
||
|
'pricelist_id': self.company_data['default_pricelist'].id,
|
||
|
})
|
||
|
|
||
|
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||
|
SaleOrderLine.create({
|
||
|
'name': 'Section',
|
||
|
'display_type': 'line_section',
|
||
|
'order_id': sale_order.id,
|
||
|
})
|
||
|
sol_prod_deliver = SaleOrderLine.create({
|
||
|
'product_id': self.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 5,
|
||
|
'order_id': sale_order.id,
|
||
|
'tax_id': False,
|
||
|
})
|
||
|
|
||
|
# Confirm the SO
|
||
|
sale_order.action_confirm()
|
||
|
|
||
|
sol_prod_deliver.write({'qty_delivered': 5.0})
|
||
|
|
||
|
# Context
|
||
|
self.context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [sale_order.id],
|
||
|
'active_id': sale_order.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
|
||
|
# Let's do an invoice with invoiceable lines
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'delivered'
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
|
||
|
invoice = sale_order.invoice_ids[0]
|
||
|
|
||
|
self.assertEqual(invoice.line_ids[0].display_type, 'line_section')
|
||
|
|
||
|
def test_qty_invoiced(self):
|
||
|
"""Verify uom rounding is correctly considered during qty_invoiced compute"""
|
||
|
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'partner_shipping_id': self.partner_a.id,
|
||
|
'pricelist_id': self.company_data['default_pricelist'].id,
|
||
|
})
|
||
|
|
||
|
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||
|
sol_prod_deliver = SaleOrderLine.create({
|
||
|
'product_id': self.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 5,
|
||
|
'order_id': sale_order.id,
|
||
|
'tax_id': False,
|
||
|
})
|
||
|
|
||
|
# Confirm the SO
|
||
|
sale_order.action_confirm()
|
||
|
|
||
|
sol_prod_deliver.write({'qty_delivered': 5.0})
|
||
|
# Context
|
||
|
self.context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [sale_order.id],
|
||
|
'active_id': sale_order.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
|
||
|
# Let's do an invoice with invoiceable lines
|
||
|
invoicing_wizard = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||
|
'advance_payment_method': 'delivered'
|
||
|
})
|
||
|
invoicing_wizard.create_invoices()
|
||
|
|
||
|
self.assertEqual(sol_prod_deliver.qty_invoiced, 5.0)
|
||
|
# We would have to change the digits of the field to
|
||
|
# test a greater decimal precision.
|
||
|
quantity = 5.13
|
||
|
move_form = Form(sale_order.invoice_ids)
|
||
|
with move_form.invoice_line_ids.edit(0) as line_form:
|
||
|
line_form.quantity = quantity
|
||
|
move_form.save()
|
||
|
|
||
|
# Default uom rounding to 0.01
|
||
|
qty_invoiced_field = sol_prod_deliver._fields.get('qty_invoiced')
|
||
|
sol_prod_deliver.env.add_to_compute(qty_invoiced_field, sol_prod_deliver)
|
||
|
self.assertEqual(sol_prod_deliver.qty_invoiced, quantity)
|
||
|
|
||
|
# Rounding to 0.1, should be rounded with UP (ceil) rounding_method
|
||
|
# Not floor or half up rounding.
|
||
|
sol_prod_deliver.product_uom.rounding *= 10
|
||
|
sol_prod_deliver.product_uom.flush_recordset(['rounding'])
|
||
|
expected_qty = 5.2
|
||
|
qty_invoiced_field = sol_prod_deliver._fields.get('qty_invoiced')
|
||
|
sol_prod_deliver.env.add_to_compute(qty_invoiced_field, sol_prod_deliver)
|
||
|
self.assertEqual(sol_prod_deliver.qty_invoiced, expected_qty)
|
||
|
|
||
|
def test_multi_company_invoice(self):
|
||
|
"""Checks that the company of the invoices generated in a multi company environment using the
|
||
|
'sale.advance.payment.inv' wizard fit with the company of the SO and not with the current company.
|
||
|
"""
|
||
|
so_company_id = self.sale_order.company_id.id
|
||
|
yet_another_company_id = self.company_data_2['company'].id
|
||
|
so_for_downpayment = self.sale_order.copy()
|
||
|
|
||
|
self.context.update(allowed_company_ids=[yet_another_company_id, self.env.company.id], company_id=yet_another_company_id)
|
||
|
context_for_downpayment = self.context.copy()
|
||
|
context_for_downpayment.update(active_ids=[so_for_downpayment.id], active_id=so_for_downpayment.id)
|
||
|
|
||
|
# Make sure the invoice is not created with a journal in the context
|
||
|
# Because it makes the test always succeed (by using the journal company instead of the env company)
|
||
|
no_journal_ctxt = dict(self.context)
|
||
|
no_journal_ctxt.pop('default_journal_id', None)
|
||
|
no_journal_ctxt.pop('journal_id', None)
|
||
|
|
||
|
self.sale_order.with_context(self.context).action_confirm()
|
||
|
payment = self.env['sale.advance.payment.inv'].with_context(no_journal_ctxt).create({
|
||
|
'advance_payment_method': 'percentage',
|
||
|
'amount': 50,
|
||
|
})
|
||
|
payment.create_invoices()
|
||
|
self.assertEqual(self.sale_order.invoice_ids[0].company_id.id, so_company_id, "The company of the invoice should be the same as the one from the SO")
|
||
|
|
||
|
so_for_downpayment.with_context(context_for_downpayment).action_confirm()
|
||
|
downpayment = self.env['sale.advance.payment.inv'].with_context(context_for_downpayment).create({
|
||
|
'advance_payment_method': 'fixed',
|
||
|
'fixed_amount': 50,
|
||
|
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||
|
})
|
||
|
downpayment.create_invoices()
|
||
|
self.assertEqual(so_for_downpayment.invoice_ids[0].company_id.id, so_company_id, "The company of the downpayment invoice should be the same as the one from the SO")
|
||
|
|
||
|
def test_invoice_analytic_distribution_model(self):
|
||
|
""" Tests whether, when an analytic account rule is set and the so has no analytic account,
|
||
|
the default analytic account is correctly computed in the invoice.
|
||
|
"""
|
||
|
analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'})
|
||
|
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
|
||
|
|
||
|
self.env['account.analytic.distribution.model'].create({
|
||
|
'analytic_distribution': {analytic_account_default.id: 100},
|
||
|
'product_id': self.product_a.id,
|
||
|
})
|
||
|
|
||
|
so_form = Form(self.env['sale.order'])
|
||
|
so_form.partner_id = self.partner_a
|
||
|
|
||
|
with so_form.order_line.new() as sol:
|
||
|
sol.product_id = self.product_a
|
||
|
sol.product_uom_qty = 1
|
||
|
|
||
|
so = so_form.save()
|
||
|
so.action_confirm()
|
||
|
so._force_lines_to_invoice_policy_order()
|
||
|
|
||
|
so_context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [so.id],
|
||
|
'active_id': so.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
down_payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({})
|
||
|
down_payment.create_invoices()
|
||
|
|
||
|
aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0]
|
||
|
self.assertRecordValues(aml, [{'analytic_distribution': {str(analytic_account_default.id): 100}}])
|
||
|
|
||
|
def test_invoice_analytic_account_so_not_default(self):
|
||
|
""" Tests whether, when an analytic account rule is set and the so has an analytic account,
|
||
|
the default analytic acount doesn't replace the one from the so in the invoice.
|
||
|
"""
|
||
|
# Required for `analytic_account_id` to be visible in the view
|
||
|
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||
|
analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'})
|
||
|
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
|
||
|
analytic_account_so = self.env['account.analytic.account'].create({'name': 'so', 'plan_id': analytic_plan_default.id})
|
||
|
|
||
|
self.env['account.analytic.distribution.model'].create({
|
||
|
'analytic_distribution': {analytic_account_default.id: 100},
|
||
|
'product_id': self.product_a.id,
|
||
|
})
|
||
|
|
||
|
so_form = Form(self.env['sale.order'])
|
||
|
so_form.partner_id = self.partner_a
|
||
|
so_form.analytic_account_id = analytic_account_so
|
||
|
|
||
|
with so_form.order_line.new() as sol:
|
||
|
sol.product_id = self.product_a
|
||
|
sol.product_uom_qty = 1
|
||
|
|
||
|
so = so_form.save()
|
||
|
so.action_confirm()
|
||
|
so._force_lines_to_invoice_policy_order()
|
||
|
|
||
|
so_context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [so.id],
|
||
|
'active_id': so.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
down_payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({})
|
||
|
down_payment.create_invoices()
|
||
|
|
||
|
aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0]
|
||
|
self.assertRecordValues(aml, [{'analytic_distribution': {str(analytic_account_default.id): 100, str(analytic_account_so.id): 100}}])
|
||
|
|
||
|
def test_invoice_analytic_rule_with_account_prefix(self):
|
||
|
"""
|
||
|
Test whether, when an analytic account rule is set within the scope (applicability) of invoice
|
||
|
and with an account prefix set,
|
||
|
the default analytic account is correctly set during the conversion from so to invoice
|
||
|
"""
|
||
|
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||
|
analytic_plan_default = self.env['account.analytic.plan'].create({
|
||
|
'name': 'default',
|
||
|
'applicability_ids': [Command.create({
|
||
|
'business_domain': 'invoice',
|
||
|
'applicability': 'optional',
|
||
|
})]
|
||
|
})
|
||
|
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
|
||
|
|
||
|
analytic_distribution_model = self.env['account.analytic.distribution.model'].create({
|
||
|
'account_prefix': '400000',
|
||
|
'analytic_distribution': {analytic_account_default.id: 100},
|
||
|
'product_id': self.product_a.id,
|
||
|
})
|
||
|
|
||
|
so = self.env['sale.order'].create({'partner_id': self.partner_a.id})
|
||
|
self.env['sale.order.line'].create({
|
||
|
'order_id': so.id,
|
||
|
'name': 'test',
|
||
|
'product_id': self.product_a.id
|
||
|
})
|
||
|
self.assertFalse(so.order_line.analytic_distribution, "There should be no tag set.")
|
||
|
so.action_confirm()
|
||
|
so.order_line.qty_delivered = 1
|
||
|
aml = so._create_invoices().invoice_line_ids
|
||
|
self.assertRecordValues(aml, [{'analytic_distribution': analytic_distribution_model.analytic_distribution}])
|
||
|
|
||
|
def test_invoice_after_product_return_price_not_default(self):
|
||
|
so = self.env['sale.order'].create({
|
||
|
'name': 'Sale order',
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
(0, 0, {'name': self.product_a.name, 'product_id': self.product_a.id, 'product_uom_qty': 1, 'price_unit': 123}),
|
||
|
]
|
||
|
})
|
||
|
self._check_order_search(so, [('invoice_ids', '=', False)], so)
|
||
|
so.action_confirm()
|
||
|
so_context = {
|
||
|
'active_model': 'sale.order',
|
||
|
'active_ids': [so.id],
|
||
|
'active_id': so.id,
|
||
|
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||
|
}
|
||
|
invoicing_wizard = self.env['sale.advance.payment.inv'].with_context(so_context).create({})
|
||
|
invoicing_wizard.create_invoices()
|
||
|
self.assertTrue(so.invoice_ids, "The invoice was not created")
|
||
|
# simulating return by changing product_uom_qty to 0
|
||
|
so.order_line.product_uom_qty = 0
|
||
|
# checking if the price_unit is the same
|
||
|
self.assertEqual(so.order_line.price_unit, 123,
|
||
|
"The unit price should be the same as the one used to create the sales order line")
|
||
|
|
||
|
def test_group_invoice(self):
|
||
|
""" Test that invoicing multiple sales order for the same customer works. """
|
||
|
# Create 3 SOs for the same partner, one of which that uses another currency
|
||
|
eur_pricelist = self.env['product.pricelist'].create({'name': 'EUR', 'currency_id': self.env.ref('base.EUR').id})
|
||
|
so1 = self.sale_order.with_context(mail_notrack=True).copy()
|
||
|
so1.pricelist_id = eur_pricelist
|
||
|
so2 = so1.copy()
|
||
|
usd_pricelist = self.env['product.pricelist'].create({'name': 'USD', 'currency_id': self.env.ref('base.USD').id})
|
||
|
so3 = so1.copy()
|
||
|
so1.pricelist_id = usd_pricelist
|
||
|
orders = so1 | so2 | so3
|
||
|
orders.action_confirm()
|
||
|
# Create the invoicing wizard and invoice all of them at once
|
||
|
wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=orders.ids, open_invoices=True).create({})
|
||
|
res = wiz.create_invoices()
|
||
|
# Check that exactly 2 invoices are generated
|
||
|
self.assertEqual(
|
||
|
len(res['domain'][0][2]),
|
||
|
2,
|
||
|
"Invoicing 3 orders for the same partner with 2 currencies"
|
||
|
"should create exactly 2 invoices.")
|
||
|
|
||
|
def test_so_note_to_invoice(self):
|
||
|
"""Test that notes from SO are pushed into invoices"""
|
||
|
|
||
|
self.sale_order.order_line = [Command.create({
|
||
|
'name': 'This is a note',
|
||
|
'display_type': 'line_note',
|
||
|
'product_id': False,
|
||
|
'product_uom_qty': 0,
|
||
|
'product_uom': False,
|
||
|
'price_unit': 0,
|
||
|
'order_id': self.sale_order.id,
|
||
|
'tax_id': False,
|
||
|
})]
|
||
|
|
||
|
# confirm quotation
|
||
|
self.sale_order.action_confirm()
|
||
|
|
||
|
# create invoice
|
||
|
invoice = self.sale_order._create_invoices()
|
||
|
|
||
|
# check note from SO has been pushed in invoice
|
||
|
self.assertEqual(
|
||
|
len(invoice.invoice_line_ids.filtered(lambda line: line.display_type == 'line_note')),
|
||
|
1,
|
||
|
'Note SO line should have been pushed to the invoice')
|
||
|
|
||
|
def test_cost_invoicing(self):
|
||
|
""" Test confirming a vendor invoice to reinvoice cost on the so """
|
||
|
serv_cost = self.env['product.product'].create({
|
||
|
'name': "Ordered at cost",
|
||
|
'standard_price': 160,
|
||
|
'list_price': 180,
|
||
|
'type': 'consu',
|
||
|
'invoice_policy': 'order',
|
||
|
'expense_policy': 'cost',
|
||
|
'default_code': 'PROD_COST',
|
||
|
'service_type': 'manual',
|
||
|
})
|
||
|
prod_gap = self.company_data['product_service_order']
|
||
|
so = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'partner_invoice_id': self.partner_a.id,
|
||
|
'partner_shipping_id': self.partner_a.id,
|
||
|
'order_line': [Command.create({
|
||
|
'product_id': prod_gap.id,
|
||
|
'product_uom_qty': 2,
|
||
|
'product_uom': prod_gap.uom_id.id,
|
||
|
'price_unit': prod_gap.list_price,
|
||
|
})],
|
||
|
'pricelist_id': self.company_data['default_pricelist'].id,
|
||
|
})
|
||
|
so.action_confirm()
|
||
|
so._create_analytic_account()
|
||
|
|
||
|
inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'invoice_date': so.date_order,
|
||
|
'invoice_line_ids': [
|
||
|
Command.create({
|
||
|
'name': serv_cost.name,
|
||
|
'product_id': serv_cost.id,
|
||
|
'product_uom_id': serv_cost.uom_id.id,
|
||
|
'quantity': 2,
|
||
|
'price_unit': serv_cost.standard_price,
|
||
|
'analytic_distribution': {so.analytic_account_id.id: 100},
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
inv.action_post()
|
||
|
sol = so.order_line.filtered(lambda l: l.product_id == serv_cost)
|
||
|
self.assertTrue(sol, 'Sale: cost invoicing does not add lines when confirming vendor invoice')
|
||
|
self.assertEqual(
|
||
|
(sol.price_unit, sol.qty_delivered, sol.product_uom_qty, sol.qty_invoiced),
|
||
|
(160, 2, 0, 0),
|
||
|
'Sale: line is wrong after confirming vendor invoice')
|
||
|
|
||
|
def test_sale_order_standard_flow_with_invoicing(self):
|
||
|
""" Test the sales order flow (invoicing and quantity updates)
|
||
|
- Invoice repeatedly while varrying delivered quantities and check that invoice are always what we expect
|
||
|
"""
|
||
|
self.sale_order.order_line.product_uom_qty = 2.0
|
||
|
# TODO?: validate invoice and register payments
|
||
|
self.sale_order.order_line.read(['name', 'price_unit', 'product_uom_qty', 'price_total'])
|
||
|
|
||
|
self.assertEqual(self.sale_order.amount_total, 1240.0, 'Sale: total amount is wrong')
|
||
|
self.sale_order.order_line._compute_product_updatable()
|
||
|
self.assertTrue(self.sale_order.order_line[0].product_updatable)
|
||
|
# send quotation
|
||
|
email_act = self.sale_order.action_quotation_send()
|
||
|
email_ctx = email_act.get('context', {})
|
||
|
self.sale_order.with_context(**email_ctx).message_post_with_source(
|
||
|
self.env['mail.template'].browse(email_ctx.get('default_template_id')),
|
||
|
subtype_xmlid='mail.mt_comment',
|
||
|
)
|
||
|
self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong')
|
||
|
self.sale_order.order_line._compute_product_updatable()
|
||
|
self.assertTrue(self.sale_order.order_line[0].product_updatable)
|
||
|
|
||
|
# confirm quotation
|
||
|
self.sale_order.action_confirm()
|
||
|
self.assertTrue(self.sale_order.state == 'sale')
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'to invoice')
|
||
|
|
||
|
# create invoice: only 'invoice on order' products are invoiced
|
||
|
invoice = self.sale_order._create_invoices()
|
||
|
self.assertEqual(len(invoice.invoice_line_ids), 2, 'Sale: invoice is missing lines')
|
||
|
self.assertEqual(invoice.amount_total, 740.0, 'Sale: invoice total amount is wrong')
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'no', 'Sale: SO status after invoicing should be "nothing to invoice"')
|
||
|
self.assertTrue(len(self.sale_order.invoice_ids) == 1, 'Sale: invoice is missing')
|
||
|
self.sale_order.order_line._compute_product_updatable()
|
||
|
self.assertFalse(self.sale_order.order_line[0].product_updatable)
|
||
|
|
||
|
# deliver lines except 'time and material' then invoice again
|
||
|
for line in self.sale_order.order_line:
|
||
|
line.qty_delivered = 2 if line.product_id.expense_policy == 'no' else 0
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'to invoice', 'Sale: SO status after delivery should be "to invoice"')
|
||
|
invoice2 = self.sale_order._create_invoices()
|
||
|
self.assertEqual(len(invoice2.invoice_line_ids), 2, 'Sale: second invoice is missing lines')
|
||
|
self.assertEqual(invoice2.amount_total, 500.0, 'Sale: second invoice total amount is wrong')
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'invoiced', 'Sale: SO status after invoicing everything should be "invoiced"')
|
||
|
self.assertTrue(len(self.sale_order.invoice_ids) == 2, 'Sale: invoice is missing')
|
||
|
|
||
|
# go over the sold quantity
|
||
|
self.sol_serv_order.write({'qty_delivered': 10})
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'upselling', 'Sale: SO status after increasing delivered qty higher than ordered qty should be "upselling"')
|
||
|
|
||
|
# upsell and invoice
|
||
|
self.sol_serv_order.write({'product_uom_qty': 10})
|
||
|
|
||
|
# There is a bug with `new` and `_origin`
|
||
|
# If you create a first new from a record, then change a value on the origin record, than create another new,
|
||
|
# this other new wont have the updated value of the origin record, but the one from the previous new
|
||
|
# Here the problem lies in the use of `new` in `move = self_ctx.new(new_vals)`,
|
||
|
# and the fact this method is called multiple times in the same transaction test case.
|
||
|
# Here, we update `qty_delivered` on the origin record, but the `new` records which are in cache with this order line
|
||
|
# as origin are not updated, nor the fields that depends on it.
|
||
|
self.env.flush_all()
|
||
|
self.env.invalidate_all()
|
||
|
|
||
|
invoice3 = self.sale_order._create_invoices()
|
||
|
self.assertEqual(len(invoice3.invoice_line_ids), 1, 'Sale: third invoice is missing lines')
|
||
|
self.assertEqual(invoice3.amount_total, 720.0, 'Sale: second invoice total amount is wrong')
|
||
|
self.assertTrue(self.sale_order.invoice_status == 'invoiced', 'Sale: SO status after invoicing everything (including the upsel) should be "invoiced"')
|
||
|
|
||
|
def test_so_create_multicompany(self):
|
||
|
"""Check that only taxes of the right company are applied on the lines."""
|
||
|
# Preparing test Data
|
||
|
product_shared = self.env['product.template'].create({
|
||
|
'name': 'shared product',
|
||
|
'invoice_policy': 'order',
|
||
|
'taxes_id': [(6, False, (self.company_data['default_tax_sale'] + self.company_data_2['default_tax_sale']).ids)],
|
||
|
'property_account_income_id': self.company_data['default_account_revenue'].id,
|
||
|
})
|
||
|
|
||
|
so_1 = self.env['sale.order'].with_user(self.company_data['default_user_salesman']).create({
|
||
|
'partner_id': self.env['res.partner'].create({'name': 'A partner'}).id,
|
||
|
'company_id': self.company_data['company'].id,
|
||
|
})
|
||
|
so_1.write({
|
||
|
'order_line': [Command.create({'product_id': product_shared.product_variant_id.id})],
|
||
|
})
|
||
|
self.assertEqual(so_1.order_line.product_uom_qty, 1)
|
||
|
|
||
|
self.assertEqual(so_1.order_line.tax_id, self.company_data['default_tax_sale'],
|
||
|
'Only taxes from the right company are put by default')
|
||
|
so_1.action_confirm()
|
||
|
# i'm not interested in groups/acls, but in the multi-company flow only
|
||
|
# the sudo is there for that and does not impact the invoice that gets created
|
||
|
# the goal here is to invoice in company 1 (because the order is in company 1) while being
|
||
|
# 'mainly' in company 2 (through the context), the invoice should be in company 1
|
||
|
inv = so_1.sudo().with_context(
|
||
|
allowed_company_ids=(self.company_data['company'] + self.company_data_2['company']).ids
|
||
|
)._create_invoices()
|
||
|
self.assertEqual(
|
||
|
inv.company_id,
|
||
|
self.company_data['company'],
|
||
|
'invoices should be created in the company of the SO, not the main company of the context')
|
||
|
|
||
|
def test_partial_invoicing_interaction_with_invoicing_switch_threshold(self):
|
||
|
""" Let's say you partially invoice a SO, let's call the resuling invoice 'A'. Now if you change the
|
||
|
'Invoicing Switch Threshold' such that the invoice date of 'A' is before the new threshold,
|
||
|
the SO should still take invoice 'A' into account.
|
||
|
"""
|
||
|
if not self.env['ir.module.module'].search([('name', '=', 'account_accountant'), ('state', '=', 'installed')]):
|
||
|
self.skipTest("This test requires the installation of the account_account module")
|
||
|
|
||
|
sale_order = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': self.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 20,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
line = sale_order.order_line[0]
|
||
|
|
||
|
sale_order.action_confirm()
|
||
|
|
||
|
line.qty_delivered = 10
|
||
|
|
||
|
invoice = sale_order._create_invoices()
|
||
|
invoice.action_post()
|
||
|
|
||
|
self.assertEqual(line.qty_invoiced, 10)
|
||
|
|
||
|
self.env['res.config.settings'].create({
|
||
|
'invoicing_switch_threshold': fields.Date.add(invoice.invoice_date, days=30),
|
||
|
}).execute()
|
||
|
|
||
|
invoice.invalidate_model(fnames=['payment_state'])
|
||
|
|
||
|
self.assertEqual(line.qty_invoiced, 10)
|
||
|
line.qty_delivered = 15
|
||
|
self.assertEqual(line.qty_invoiced, 10)
|
||
|
|
||
|
def test_salesperson_in_invoice_followers(self):
|
||
|
"""
|
||
|
Test if the salesperson is in the followers list of invoice created from SO
|
||
|
"""
|
||
|
# create a salesperson
|
||
|
salesperson = self.env['res.users'].create({
|
||
|
'name': 'Salesperson',
|
||
|
'login': 'salesperson',
|
||
|
'email': 'test@test.com',
|
||
|
'groups_id': [(6, 0, [self.env.ref('sales_team.group_sale_salesman').id])]
|
||
|
})
|
||
|
|
||
|
# create a SO and generate invoice from it
|
||
|
sale_order = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'user_id': salesperson.id,
|
||
|
'order_line': [(0, 0, {
|
||
|
'product_id': self.company_data['product_order_no'].id,
|
||
|
'product_uom_qty': 1,
|
||
|
})]
|
||
|
})
|
||
|
sale_order.action_confirm()
|
||
|
invoice = sale_order._create_invoices(final=True)
|
||
|
|
||
|
# check if the salesperson is in the followers list of invoice created from SO
|
||
|
self.assertIn(salesperson.partner_id, invoice.message_partner_ids, 'Salesperson not in the followers list of '
|
||
|
'invoice created from SO')
|
||
|
def test_amount_to_invoice_multiple_so(self):
|
||
|
""" Testing creating two SOs with the same customer and invoicing them together. We have to ensure
|
||
|
that the amount to invoice is correct for each SO.
|
||
|
"""
|
||
|
sale_order_1 = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': self.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 10,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
sale_order_2 = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': self.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 20,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
|
||
|
sale_order_1.action_confirm()
|
||
|
sale_order_2.action_confirm()
|
||
|
sale_order_1.order_line.qty_delivered = 10
|
||
|
sale_order_2.order_line.qty_delivered = 20
|
||
|
|
||
|
self.env['sale.advance.payment.inv'].create({
|
||
|
'advance_payment_method': 'delivered',
|
||
|
'sale_order_ids': [Command.set((sale_order_1 + sale_order_2).ids)],
|
||
|
}).create_invoices()
|
||
|
|
||
|
sale_order_1.invoice_ids.action_post()
|
||
|
|
||
|
self.assertEqual(sale_order_1.amount_to_invoice, 0.0)
|
||
|
self.assertEqual(sale_order_2.amount_to_invoice, 0.0)
|
||
|
|
||
|
def test_amount_to_invoice_one_line_multiple_so(self):
|
||
|
""" Testing creating two SOs linked to the same invoice line. Drawback: the substracted
|
||
|
amount to the amount_total will take both sale order into account.
|
||
|
"""
|
||
|
sale_order_1 = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': self.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 10,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
sale_order_2 = self.env['sale.order'].create({
|
||
|
'partner_id': self.partner_a.id,
|
||
|
'order_line': [
|
||
|
Command.create({
|
||
|
'product_id': self.company_data['product_delivery_no'].id,
|
||
|
'product_uom_qty': 20,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
|
||
|
sale_order_1.action_confirm()
|
||
|
sale_order_2.action_confirm()
|
||
|
sale_order_1.order_line.qty_delivered = 10
|
||
|
sale_order_2.order_line.qty_delivered = 20
|
||
|
|
||
|
self.env['sale.advance.payment.inv'].create({
|
||
|
'advance_payment_method': 'delivered',
|
||
|
'sale_order_ids': [Command.set((sale_order_2).ids)],
|
||
|
}).create_invoices()
|
||
|
|
||
|
sale_order_1.invoice_ids = sale_order_2.invoice_ids
|
||
|
sale_order_1.invoice_ids.line_ids.sale_line_ids += sale_order_1.order_line
|
||
|
|
||
|
sale_order_1.invoice_ids.action_post()
|
||
|
|
||
|
self.assertEqual(sale_order_1.amount_to_invoice, -700.0)
|
||
|
self.assertEqual(sale_order_2.amount_to_invoice, 0.0)
|