mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 22:52:03 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
21
odoo-bringout-oca-ocb-sale/sale/tests/__init__.py
Normal file
21
odoo-bringout-oca-ocb-sale/sale/tests/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_access_rights
|
||||
from . import test_accrued_sale_orders
|
||||
from . import test_common
|
||||
from . import test_controllers
|
||||
from . import test_credit_limit
|
||||
from . import test_onchange
|
||||
from . import test_payment_flow
|
||||
from . import test_reinvoice
|
||||
from . import test_sale_flow
|
||||
from . import test_sale_onboarding
|
||||
from . import test_sale_order
|
||||
from . import test_sale_order_cancel
|
||||
from . import test_sale_prices
|
||||
from . import test_sale_product_attribute_value_config
|
||||
from . import test_sale_refund
|
||||
from . import test_sale_tax_totals
|
||||
from . import test_sale_to_invoice
|
||||
from . import test_sale_report
|
||||
240
odoo-bringout-oca-ocb-sale/sale/tests/common.py
Normal file
240
odoo-bringout-oca-ocb-sale/sale/tests/common.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import TransactionCase
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.product.tests.common import ProductCommon
|
||||
from odoo.addons.sales_team.tests.common import SalesTeamCommon
|
||||
|
||||
|
||||
class SaleCommon(
|
||||
ProductCommon, # BaseCommon, UomCommon
|
||||
SalesTeamCommon,
|
||||
):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.company.country_id = cls.env.ref('base.us')
|
||||
|
||||
# Not defined in product common because only used in sale
|
||||
cls.group_discount_per_so_line = cls.env.ref('product.group_discount_per_so_line')
|
||||
|
||||
cls.empty_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
})
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': cls.consumable_product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.service_product.id,
|
||||
'product_uom_qty': 12.5,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _enable_pricelists(cls):
|
||||
cls.env.user.groups_id += cls.env.ref('product.group_product_pricelist')
|
||||
|
||||
|
||||
class TestSaleCommonBase(TransactionCase):
|
||||
''' Setup with sale test configuration. '''
|
||||
|
||||
@classmethod
|
||||
def setup_sale_configuration_for_company(cls, company):
|
||||
Users = cls.env['res.users'].with_context(no_reset_password=True)
|
||||
|
||||
company_data = {
|
||||
# Sales Team
|
||||
'default_sale_team': cls.env['crm.team'].with_context(tracking_disable=True).create({
|
||||
'name': 'Test Channel',
|
||||
'company_id': company.id,
|
||||
}),
|
||||
|
||||
# Users
|
||||
'default_user_salesman': Users.create({
|
||||
'name': 'default_user_salesman',
|
||||
'login': 'default_user_salesman.comp%s' % company.id,
|
||||
'email': 'default_user_salesman@example.com',
|
||||
'signature': '--\nMark',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, cls.env.ref('sales_team.group_sale_salesman').ids)],
|
||||
'company_ids': [(6, 0, company.ids)],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'default_user_portal': Users.create({
|
||||
'name': 'default_user_portal',
|
||||
'login': 'default_user_portal.comp%s' % company.id,
|
||||
'email': 'default_user_portal@gladys.portal',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])],
|
||||
'company_ids': [(6, 0, company.ids)],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'default_user_employee': Users.create({
|
||||
'name': 'default_user_employee',
|
||||
'login': 'default_user_employee.comp%s' % company.id,
|
||||
'email': 'default_user_employee@example.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
|
||||
'company_ids': [(6, 0, company.ids)],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
|
||||
# Pricelist
|
||||
'default_pricelist': cls.env['product.pricelist'].with_company(company).create({
|
||||
'name': 'default_pricelist',
|
||||
'currency_id': company.currency_id.id,
|
||||
}),
|
||||
|
||||
# Product category
|
||||
'product_category': cls.env['product.category'].with_company(company).create({
|
||||
'name': 'Test category',
|
||||
}),
|
||||
}
|
||||
|
||||
company_data.update({
|
||||
# Products
|
||||
'product_service_delivery': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_service_delivery',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 200.0,
|
||||
'list_price': 180.0,
|
||||
'type': 'service',
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'SERV_DEL',
|
||||
'invoice_policy': 'delivery',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_service_order': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_service_order',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 40.0,
|
||||
'list_price': 90.0,
|
||||
'type': 'service',
|
||||
'uom_id': cls.env.ref('uom.product_uom_hour').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_hour').id,
|
||||
'description': 'Example of product to invoice on order',
|
||||
'default_code': 'PRE-PAID',
|
||||
'invoice_policy': 'order',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_order_cost': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_cost',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 235.0,
|
||||
'list_price': 280.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'cost',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_delivery_cost': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_cost',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 55.0,
|
||||
'list_price': 70.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'cost',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_order_sales_price': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_sales_price',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 235.0,
|
||||
'list_price': 280.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'sales_price',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_delivery_sales_price': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_sales_price',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 55.0,
|
||||
'list_price': 70.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'sales_price',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_order_no': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_no',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 235.0,
|
||||
'list_price': 280.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'no',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
'product_delivery_no': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_no',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'standard_price': 55.0,
|
||||
'list_price': 70.0,
|
||||
'type': 'consu',
|
||||
'weight': 0.01,
|
||||
'uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'uom_po_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'no',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
}),
|
||||
})
|
||||
|
||||
return company_data
|
||||
|
||||
|
||||
class TestSaleCommon(AccountTestInvoicingCommon, TestSaleCommonBase):
|
||||
''' Setup to be used post-install with sale and accounting test configuration.'''
|
||||
|
||||
@classmethod
|
||||
def setup_company_data(cls, company_name, chart_template=None, **kwargs):
|
||||
company_data = super().setup_company_data(company_name, chart_template=chart_template, **kwargs)
|
||||
|
||||
company_data.update(cls.setup_sale_configuration_for_company(company_data['company']))
|
||||
|
||||
company_data['product_category'].write({
|
||||
'property_account_income_categ_id': company_data['default_account_revenue'].id,
|
||||
'property_account_expense_categ_id': company_data['default_account_expense'].id,
|
||||
})
|
||||
|
||||
return company_data
|
||||
138
odoo-bringout-oca-ocb-sale/sale/tests/test_access_rights.py
Normal file
138
odoo-bringout-oca-ocb-sale/sale/tests/test_access_rights.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.base.tests.common import BaseUsersCommon
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccessRights(BaseUsersCommon, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.sale_user2 = cls.env['res.users'].create({
|
||||
'name': 'salesman_2',
|
||||
'login': 'salesman_2',
|
||||
'email': 'default_user_salesman_2@example.com',
|
||||
'signature': '--\nMark',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, cls.group_sale_salesman.ids)],
|
||||
})
|
||||
|
||||
# Create the SO with a specific salesperson
|
||||
cls.sale_order.user_id = cls.sale_user
|
||||
|
||||
def test_access_sales_manager(self):
|
||||
""" Test sales manager's access rights """
|
||||
SaleOrder = self.env['sale.order'].with_user(self.sale_manager)
|
||||
so_as_sale_manager = SaleOrder.browse(self.sale_order.id)
|
||||
|
||||
# Manager can see the SO which is assigned to another salesperson
|
||||
so_as_sale_manager.read()
|
||||
# Manager can change a salesperson of the SO
|
||||
so_as_sale_manager.write({'user_id': self.sale_user2.id})
|
||||
|
||||
# Manager can create the SO for other salesperson
|
||||
sale_order = SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
'user_id': self.sale_user.id
|
||||
})
|
||||
self.assertIn(
|
||||
sale_order.id, SaleOrder.search([]).ids,
|
||||
'Sales manager should be able to create the SO of other salesperson')
|
||||
# Manager can confirm the SO
|
||||
sale_order.action_confirm()
|
||||
# Manager can not delete confirmed SO
|
||||
with self.assertRaises(UserError), mute_logger('odoo.models.unlink'):
|
||||
sale_order.unlink()
|
||||
|
||||
# Manager can delete the SO of other salesperson if SO is in 'draft' or 'cancel' state
|
||||
so_as_sale_manager.unlink()
|
||||
self.assertNotIn(
|
||||
so_as_sale_manager.id, SaleOrder.search([]).ids,
|
||||
'Sales manager should be able to delete the SO')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_access_sales_person(self):
|
||||
""" Test Salesperson's access rights """
|
||||
SaleOrder = self.env['sale.order'].with_user(self.sale_user2)
|
||||
so_as_salesperson = SaleOrder.browse(self.sale_order.id)
|
||||
|
||||
# Salesperson can see only their own sales order
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_salesperson.read()
|
||||
|
||||
# Now assign the SO to themselves
|
||||
# (using self.sale_order to do the change as superuser)
|
||||
self.sale_order.write({'user_id': self.sale_user2.id})
|
||||
|
||||
# The salesperson is now able to read it
|
||||
so_as_salesperson.read()
|
||||
# Salesperson can change a Sales Team of SO
|
||||
so_as_salesperson.write({'team_id': self.sale_team.id})
|
||||
|
||||
# Salesperson can't create a SO for other salesperson
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['sale.order'].with_user(self.sale_user2).create({
|
||||
'partner_id': self.partner.id,
|
||||
'user_id': self.sale_user.id
|
||||
})
|
||||
|
||||
# Salesperson can't delete Sale Orders
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_salesperson.unlink()
|
||||
|
||||
# Salesperson can confirm the SO
|
||||
so_as_salesperson.action_confirm()
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_access_portal_user(self):
|
||||
""" Test portal user's access rights """
|
||||
SaleOrder = self.env['sale.order'].with_user(self.user_portal)
|
||||
so_as_portal_user = SaleOrder.browse(self.sale_order.id)
|
||||
|
||||
# Portal user can see the confirmed SO for which they are assigned as a customer
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_portal_user.read()
|
||||
|
||||
self.sale_order.partner_id = self.user_portal.partner_id
|
||||
self.sale_order.action_confirm()
|
||||
# Portal user can't edit the SO
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_portal_user.write({'team_id': self.sale_team.id})
|
||||
# Portal user can't create the SO
|
||||
with self.assertRaises(AccessError):
|
||||
SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
# Portal user can't delete the SO which is in 'draft' or 'cancel' state
|
||||
self.sale_order.action_cancel()
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_portal_user.unlink()
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_access_employee(self):
|
||||
""" Test classic employee's access rights """
|
||||
SaleOrder = self.env['sale.order'].with_user(self.user_internal)
|
||||
so_as_internal_user = SaleOrder.browse(self.sale_order.id)
|
||||
|
||||
# Employee can't see any SO
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_internal_user.read()
|
||||
# Employee can't edit the SO
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_internal_user.write({'team_id': self.sale_team.id})
|
||||
# Employee can't create the SO
|
||||
with self.assertRaises(AccessError):
|
||||
SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
# Employee can't delete the SO
|
||||
with self.assertRaises(AccessError):
|
||||
so_as_internal_user.unlink()
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from freezegun import freeze_time
|
||||
from odoo import Command
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
@freeze_time('2022-01-01')
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
cls.alt_inc_account = cls.company_data['default_account_revenue'].copy()
|
||||
# set 'invoice_policy' to 'delivery' to take 'qty_delivered' into account when computing 'untaxed_amount_to_invoice'
|
||||
# set 'type' to 'service' to allow manualy set 'qty_delivered' even with sale_stock installed
|
||||
cls.product_a.update({
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
})
|
||||
cls.product_b.update({
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'property_account_income_id': cls.alt_inc_account.id,
|
||||
})
|
||||
cls.default_plan = cls.env['account.analytic.plan'].create({'name': 'Default', 'company_id': False})
|
||||
cls.analytic_account_a = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_a',
|
||||
'plan_id': cls.default_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account_b = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_b',
|
||||
'plan_id': cls.default_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account_c = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_c',
|
||||
'plan_id': cls.default_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'name': cls.product_a.name,
|
||||
'product_id': cls.product_a.id,
|
||||
'product_uom_qty': 10.0,
|
||||
'product_uom': cls.product_a.uom_id.id,
|
||||
'price_unit': cls.product_a.list_price,
|
||||
'tax_id': False,
|
||||
'analytic_distribution': {
|
||||
cls.analytic_account_a.id : 80.0,
|
||||
cls.analytic_account_b.id : 20.0,
|
||||
},
|
||||
}),
|
||||
Command.create({
|
||||
'name': cls.product_b.name,
|
||||
'product_id': cls.product_b.id,
|
||||
'product_uom_qty': 10.0,
|
||||
'product_uom': cls.product_b.uom_id.id,
|
||||
'price_unit': cls.product_b.list_price,
|
||||
'tax_id': False,
|
||||
'analytic_distribution': {
|
||||
cls.analytic_account_b.id : 100.0,
|
||||
},
|
||||
})
|
||||
]
|
||||
})
|
||||
cls.sale_order.analytic_account_id = cls.analytic_account_c
|
||||
cls.sale_order.action_confirm()
|
||||
cls.account_expense = cls.company_data['default_account_expense']
|
||||
cls.account_revenue = cls.company_data['default_account_revenue']
|
||||
cls.wizard = cls.env['account.accrued.orders.wizard'].with_context({
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': cls.sale_order.ids,
|
||||
}).create({
|
||||
'account_id': cls.account_expense.id,
|
||||
})
|
||||
|
||||
def test_accrued_order(self):
|
||||
# nothing to invoice : no entries to be created
|
||||
with self.assertRaises(UserError):
|
||||
self.wizard.create_entries()
|
||||
|
||||
# 5 qty of each product invoiceable
|
||||
self.sale_order.order_line.qty_delivered = 5
|
||||
self.assertRecordValues(self.env['account.move'].search(self.wizard.create_entries()['domain']).line_ids, [
|
||||
# reverse move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 5000, 'credit': 0},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 1000, 'credit': 0},
|
||||
{'account_id': self.wizard.account_id.id, 'debit': 0, 'credit': 6000},
|
||||
# move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 5000},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 0, 'credit': 1000},
|
||||
{'account_id': self.wizard.account_id.id, 'debit': 6000, 'credit': 0},
|
||||
])
|
||||
|
||||
# delivered products invoiced, nothing to invoice left
|
||||
self.sale_order.with_context(default_invoice_date=self.wizard.date)._create_invoices().action_post()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.move.line']._invalidate_cache()
|
||||
self.wizard.create_entries()
|
||||
self.assertTrue(self.wizard.display_amount)
|
||||
|
||||
def test_multi_currency_accrued_order(self):
|
||||
# 5 qty of each product billeable
|
||||
self.sale_order.order_line.qty_delivered = 5
|
||||
# self.sale_order.order_line.product_uom_qty = 5
|
||||
# set currency != company currency
|
||||
self.sale_order.currency_id = self.currency_data['currency']
|
||||
self.assertRecordValues(self.env['account.move'].search(self.wizard.create_entries()['domain']).line_ids, [
|
||||
# reverse move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 5000 / 2, 'credit': 0, 'amount_currency': 5000},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 1000 / 2, 'credit': 0, 'amount_currency': 1000},
|
||||
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 6000 / 2, 'amount_currency': 0.0},
|
||||
# move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 5000 / 2, 'amount_currency': -5000},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 0, 'credit': 1000 / 2, 'amount_currency': -1000},
|
||||
{'account_id': self.account_expense.id, 'debit': 6000 / 2, 'credit': 0, 'amount_currency': 0.0},
|
||||
])
|
||||
|
||||
def test_analytic_account_accrued_order(self):
|
||||
self.sale_order.order_line.qty_delivered = 10
|
||||
|
||||
self.assertRecordValues(self.env['account.move'].search(self.wizard.create_entries()['domain']).line_ids, [
|
||||
# reverse move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 10000.0, 'credit': 0.0, 'analytic_distribution': {str(self.analytic_account_a.id): 80.0, str(self.analytic_account_b.id): 20.0, str(self.analytic_account_c.id): 100.0}},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 2000.0, 'credit': 0.0, 'analytic_distribution': {str(self.analytic_account_b.id): 100.0, str(self.analytic_account_c.id): 100.0}},
|
||||
{'account_id': self.account_expense.id, 'debit': 0.0, 'credit': 12000.0, 'analytic_distribution': {str(self.analytic_account_a.id): 66.67, str(self.analytic_account_b.id): 33.33, str(self.analytic_account_c.id): 100.0}},
|
||||
# move lines
|
||||
{'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 10000.0, 'analytic_distribution': {str(self.analytic_account_a.id): 80.0, str(self.analytic_account_b.id): 20.0, str(self.analytic_account_c.id): 100.0}},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 0.0, 'credit': 2000.0, 'analytic_distribution': {str(self.analytic_account_b.id): 100.0, str(self.analytic_account_c.id): 100.0}},
|
||||
{'account_id': self.account_expense.id, 'debit': 12000.0, 'credit': 0.0, 'analytic_distribution': {str(self.analytic_account_a.id): 66.67, str(self.analytic_account_b.id): 33.33, str(self.analytic_account_c.id): 100.0}},
|
||||
|
||||
])
|
||||
48
odoo-bringout-oca-ocb-sale/sale/tests/test_common.py
Normal file
48
odoo-bringout-oca-ocb-sale/sale/tests/test_common.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleCommon(SaleCommon):
|
||||
|
||||
def test_common(self):
|
||||
self.assertFalse(self.empty_order.order_line)
|
||||
self.assertEqual(self.empty_order.amount_total, 0.0)
|
||||
|
||||
self.assertEqual(self.empty_order.partner_id, self.partner)
|
||||
self.assertEqual(self.empty_order.partner_invoice_id, self.partner)
|
||||
self.assertEqual(self.empty_order.partner_shipping_id, self.partner)
|
||||
self.assertEqual(self.empty_order.pricelist_id, self.pricelist)
|
||||
self.assertEqual(self.empty_order.currency_id.name, 'USD')
|
||||
self.assertEqual(self.empty_order.team_id, self.sale_team)
|
||||
self.assertEqual(self.empty_order.state, 'draft')
|
||||
|
||||
self.assertEqual(self.sale_order.partner_id, self.partner)
|
||||
self.assertEqual(self.sale_order.partner_invoice_id, self.partner)
|
||||
self.assertEqual(self.sale_order.partner_shipping_id, self.partner)
|
||||
self.assertEqual(self.sale_order.pricelist_id, self.pricelist)
|
||||
self.assertEqual(self.sale_order.currency_id.name, 'USD')
|
||||
self.assertEqual(self.sale_order.team_id, self.sale_team)
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
|
||||
consumable_line, service_line = self.sale_order.order_line
|
||||
|
||||
self.assertFalse(consumable_line.pricelist_item_id)
|
||||
self.assertEqual(consumable_line.price_unit, 20.0)
|
||||
self.assertEqual(consumable_line.price_reduce, 20.0)
|
||||
self.assertFalse(consumable_line.discount)
|
||||
self.assertEqual(consumable_line.product_uom, self.uom_unit)
|
||||
self.assertEqual(consumable_line.price_total, 5.0 * 20.0)
|
||||
|
||||
self.assertFalse(service_line.pricelist_item_id)
|
||||
self.assertEqual(service_line.price_unit, 50.0)
|
||||
self.assertEqual(service_line.price_reduce, 50.0)
|
||||
self.assertFalse(service_line.discount)
|
||||
self.assertEqual(service_line.product_uom, self.uom_unit)
|
||||
self.assertEqual(service_line.price_total, 12.5 * 50)
|
||||
|
||||
self.assertEqual(self.sale_order.amount_total, 725.0)
|
||||
110
odoo-bringout-oca-ocb-sale/sale/tests/test_controllers.py
Normal file
110
odoo-bringout-oca-ocb-sale/sale/tests/test_controllers.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.base.tests.common import BaseUsersCommon, HttpCaseWithUserPortal
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccessRightsControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_access_controller(self):
|
||||
private_so = self.sale_order
|
||||
portal_so = self.sale_order.copy()
|
||||
portal_so.message_subscribe(self.user_portal.partner_id.ids)
|
||||
|
||||
portal_so._portal_ensure_token()
|
||||
token = portal_so.access_token
|
||||
|
||||
self.authenticate(None, None)
|
||||
|
||||
# Test public user can't print an order without a token
|
||||
req = self.url_open(
|
||||
url='/my/orders/%s?report_type=pdf' % portal_so.id,
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertEqual(req.status_code, 303)
|
||||
|
||||
# or with a random token
|
||||
req = self.url_open(
|
||||
url='/my/orders/%s?access_token=%s&report_type=pdf' % (
|
||||
portal_so.id,
|
||||
"foo",
|
||||
),
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertEqual(req.status_code, 303)
|
||||
|
||||
# but works fine with the right token
|
||||
req = self.url_open(
|
||||
url='/my/orders/%s?access_token=%s&report_type=pdf' % (
|
||||
portal_so.id,
|
||||
token,
|
||||
),
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertEqual(req.status_code, 200)
|
||||
|
||||
self.authenticate(self.user_portal.login, self.user_portal.login)
|
||||
|
||||
# do not need the token when logged in
|
||||
req = self.url_open(
|
||||
url='/my/orders/%s?report_type=pdf' % portal_so.id,
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertEqual(req.status_code, 200)
|
||||
|
||||
# but still can't access another order
|
||||
req = self.url_open(
|
||||
url='/my/orders/%s?report_type=pdf' % private_so.id,
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertEqual(req.status_code, 303)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSalesControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
||||
def test_sales_portal_report(self):
|
||||
portal_so = self.sale_order.copy()
|
||||
portal_so.message_subscribe(self.user_portal.partner_id.ids)
|
||||
|
||||
self.authenticate(None, None)
|
||||
|
||||
req = self.url_open(portal_so.get_portal_url(report_type='pdf'), allow_redirects=False)
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.headers['content-disposition'], f"inline; filename*=UTF-8''Quotation-{portal_so.name}.pdf")
|
||||
|
||||
req = self.url_open(portal_so.get_portal_url(report_type='pdf', download=True), allow_redirects=False)
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.headers['content-disposition'], f"attachment; filename*=UTF-8''Quotation-{portal_so.name}.pdf")
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleSignature(HttpCaseWithUserPortal):
|
||||
|
||||
def test_01_portal_sale_signature_tour(self):
|
||||
"""The goal of this test is to make sure the portal user can sign SO."""
|
||||
|
||||
portal_user_partner = self.partner_portal
|
||||
# create a SO to be signed
|
||||
sales_order = self.env['sale.order'].create({
|
||||
'name': 'test SO',
|
||||
'partner_id': portal_user_partner.id,
|
||||
'state': 'sent',
|
||||
'require_payment': False,
|
||||
})
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': sales_order.id,
|
||||
'product_id': self.env['product.product'].create({'name': 'A product'}).id,
|
||||
})
|
||||
|
||||
# must be sent to the user so he can see it
|
||||
email_act = sales_order.action_quotation_send()
|
||||
email_ctx = email_act.get('context', {})
|
||||
sales_order.with_context(**email_ctx).message_post_with_template(
|
||||
email_ctx.get('default_template_id'))
|
||||
|
||||
self.start_tour("/", 'sale_signature', login="portal")
|
||||
101
odoo-bringout-oca-ocb-sale/sale/tests/test_credit_limit.py
Normal file
101
odoo-bringout-oca-ocb-sale/sale/tests/test_credit_limit.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import Form, tagged, users
|
||||
|
||||
from .common import TestSaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleOrderCreditLimit(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.company.account_use_credit_limit = True
|
||||
|
||||
buck_currency = cls.env['res.currency'].create({
|
||||
'name': 'TB',
|
||||
'symbol': 'TB',
|
||||
})
|
||||
cls.env['res.currency.rate'].create({
|
||||
'name': '2023-01-01',
|
||||
'rate': 2.0,
|
||||
'currency_id': buck_currency.id,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
|
||||
cls.buck_pricelist = cls.env['product.pricelist'].create({
|
||||
'name': 'Test Buck Pricelist',
|
||||
'currency_id': buck_currency.id,
|
||||
})
|
||||
|
||||
cls.sales_user = cls.company_data['default_user_salesman']
|
||||
cls.sales_user.write({
|
||||
'login': "notaccountman",
|
||||
'email': "bad@accounting.com",
|
||||
})
|
||||
|
||||
cls.empty_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
})
|
||||
|
||||
def test_credit_limit_multicurrency(self):
|
||||
self.partner_a.credit_limit = 50
|
||||
|
||||
order = self.empty_order
|
||||
order.write({
|
||||
'pricelist_id': self.buck_pricelist.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 45.0,
|
||||
'tax_id': False,
|
||||
})
|
||||
]
|
||||
})
|
||||
self.assertEqual(order.amount_total / order.currency_rate, 22.5)
|
||||
self.assertEqual(order.partner_credit_warning, '')
|
||||
|
||||
order.write({
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 65.0,
|
||||
'tax_id': False,
|
||||
})
|
||||
],
|
||||
})
|
||||
self.assertEqual(order.amount_total / order.currency_rate, 55)
|
||||
self.assertEqual(
|
||||
order.partner_credit_warning,
|
||||
"partner_a has reached its Credit Limit of : $\xa050.00\n"
|
||||
"Total amount due (including this document) : $\xa055.00"
|
||||
)
|
||||
|
||||
@users('notaccountman')
|
||||
def test_credit_limit_access(self):
|
||||
"""Ensure credit warning gets displayed without Accounting access."""
|
||||
self.empty_order.user_id = self.env.user
|
||||
self.empty_order.partner_id.credit_limit = self.product_a.list_price
|
||||
|
||||
for group in self.partner_a._fields['credit'].groups.split(','):
|
||||
self.assertFalse(self.env.user.has_group(group))
|
||||
|
||||
with Form(self.empty_order.with_env(self.env)) as order_form:
|
||||
with order_form.order_line.new() as sol:
|
||||
sol.product_id = self.product_a
|
||||
sol.tax_id.clear()
|
||||
self.assertFalse(
|
||||
order_form.partner_credit_warning,
|
||||
"No credit warning should be displayed (yet)",
|
||||
)
|
||||
with order_form.order_line.edit(0) as sol:
|
||||
sol.tax_id.add(self.product_a.taxes_id)
|
||||
self.assertTrue(
|
||||
order_form.partner_credit_warning,
|
||||
"Credit warning should be displayed",
|
||||
)
|
||||
87
odoo-bringout-oca-ocb-sale/sale/tests/test_onchange.py
Normal file
87
odoo-bringout-oca-ocb-sale/sale/tests/test_onchange.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleOnchanges(TransactionCase):
|
||||
|
||||
def test_sale_warnings(self):
|
||||
"""Test warnings & SO/SOL updates when partner/products with sale warnings are used."""
|
||||
partner_with_warning = self.env['res.partner'].create({
|
||||
'name': 'Test', 'sale_warn': 'warning', 'sale_warn_msg': 'Highly infectious disease'})
|
||||
partner_with_block_warning = self.env['res.partner'].create({
|
||||
'name': 'Test2', 'sale_warn': 'block', 'sale_warn_msg': 'Cannot afford our services'})
|
||||
|
||||
sale_order = self.env['sale.order'].create({'partner_id': partner_with_warning.id})
|
||||
warning = sale_order._onchange_partner_id_warning()
|
||||
self.assertDictEqual(warning, {
|
||||
'warning': {
|
||||
'title': "Warning for Test",
|
||||
'message': partner_with_warning.sale_warn_msg,
|
||||
},
|
||||
})
|
||||
|
||||
sale_order.partner_id = partner_with_block_warning
|
||||
warning = sale_order._onchange_partner_id_warning()
|
||||
self.assertDictEqual(warning, {
|
||||
'warning': {
|
||||
'title': "Warning for Test2",
|
||||
'message': partner_with_block_warning.sale_warn_msg,
|
||||
},
|
||||
})
|
||||
|
||||
# Verify partner-related fields have been correctly reset
|
||||
self.assertFalse(sale_order.partner_id.id)
|
||||
self.assertFalse(sale_order.partner_invoice_id.id)
|
||||
self.assertFalse(sale_order.partner_shipping_id.id)
|
||||
self.assertFalse(sale_order.pricelist_id.id)
|
||||
|
||||
# Reuse non blocking partner for product warning tests
|
||||
sale_order.partner_id = partner_with_warning
|
||||
product_with_warning = self.env['product.product'].create({
|
||||
'name': 'Test Product', 'sale_line_warn': 'warning', 'sale_line_warn_msg': 'Highly corrosive'})
|
||||
product_with_block_warning = self.env['product.product'].create({
|
||||
'name': 'Test Product (2)', 'sale_line_warn': 'block', 'sale_line_warn_msg': 'Not produced anymore'})
|
||||
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': product_with_warning.id,
|
||||
})
|
||||
warning = sale_order_line._onchange_product_id_warning()
|
||||
self.assertDictEqual(warning, {
|
||||
'warning': {
|
||||
'title': "Warning for Test Product",
|
||||
'message': product_with_warning.sale_line_warn_msg,
|
||||
},
|
||||
})
|
||||
|
||||
sale_order_line.product_id = product_with_block_warning
|
||||
warning = sale_order_line._onchange_product_id_warning()
|
||||
|
||||
self.assertDictEqual(warning, {
|
||||
'warning': {
|
||||
'title': "Warning for Test Product (2)",
|
||||
'message': product_with_block_warning.sale_line_warn_msg,
|
||||
},
|
||||
})
|
||||
|
||||
self.assertFalse(sale_order_line.product_id.id)
|
||||
|
||||
def test_create_products_in_different_companies(self):
|
||||
""" Ensures the product's constrain on `company_id` doesn't block the creation of multiple
|
||||
products in different companies (see `product.template` `_check_sale_product_company`.)
|
||||
"""
|
||||
company_a = self.env['res.company'].create({'name': 'Company A'})
|
||||
company_b = self.env['res.company'].create({'name': 'Company B'})
|
||||
products = self.env['product.template'].create([
|
||||
{'name': "Product Test 1", 'company_id': company_a.id},
|
||||
{'name': "Product Test 2", 'company_id': company_b.id},
|
||||
{'name': "Product Test 3", 'company_id': False},
|
||||
])
|
||||
self.assertRecordValues(products, [
|
||||
{'company_id': company_a.id},
|
||||
{'company_id': company_b.id},
|
||||
{'company_id': False},
|
||||
])
|
||||
273
odoo-bringout-oca-ocb-sale/sale/tests/test_payment_flow.py
Normal file
273
odoo-bringout-oca-ocb-sale/sale/tests/test_payment_flow.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Replace PaymentCommon defaults by SaleCommon ones
|
||||
cls.currency = cls.sale_order.currency_id
|
||||
cls.partner = cls.sale_order.partner_invoice_id
|
||||
|
||||
def test_11_so_payment_link(self):
|
||||
# test customized /payment/pay route with sale_order_id param
|
||||
self.amount = self.sale_order.amount_total
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['sale_order_id'] = self.sale_order.id
|
||||
|
||||
with patch(
|
||||
'odoo.addons.payment.controllers.portal.PaymentPortal'
|
||||
'._compute_show_tokenize_input_mapping'
|
||||
) as patched:
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
patched.assert_called_once_with(ANY, logged_in=ANY, sale_order_id=ANY)
|
||||
|
||||
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
|
||||
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
|
||||
self.assertEqual(tx_context['amount'], self.sale_order.amount_total)
|
||||
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
|
||||
|
||||
route_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'validation_route': False,
|
||||
'reference_prefix': None, # Force empty prefix to fallback on SO reference
|
||||
'landing_route': tx_context['landing_route'],
|
||||
'amount': tx_context['amount'],
|
||||
'currency_id': tx_context['currency_id'],
|
||||
})
|
||||
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
|
||||
self.assertEqual(tx_sudo.amount, self.amount)
|
||||
self.assertEqual(tx_sudo.partner_id, self.sale_order.partner_invoice_id)
|
||||
self.assertEqual(tx_sudo.company_id, self.sale_order.company_id)
|
||||
self.assertEqual(tx_sudo.currency_id, self.sale_order.currency_id)
|
||||
self.assertEqual(tx_sudo.reference, self.sale_order.name)
|
||||
|
||||
# Check validation of transaction correctly confirms the SO
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
|
||||
tx_sudo._set_done()
|
||||
tx_sudo._finalize_post_processing()
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(tx_sudo.payment_id)
|
||||
self.assertEqual(tx_sudo.payment_id.state, 'posted')
|
||||
|
||||
def test_so_payment_link_with_different_partner_invoice(self):
|
||||
# test customized /payment/pay route with sale_order_id param
|
||||
# partner_id and partner_invoice_id different on the so
|
||||
self.sale_order.partner_invoice_id = self.portal_partner
|
||||
self.partner = self.sale_order.partner_invoice_id
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['sale_order_id'] = self.sale_order.id
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
|
||||
|
||||
def test_12_so_partial_payment_link(self):
|
||||
# test customized /payment/pay route with sale_order_id param
|
||||
# partial amount specified
|
||||
self.amount = self.sale_order.amount_total / 2.0
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['sale_order_id'] = self.sale_order.id
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
|
||||
self.assertEqual(tx_context['reference_prefix'], self.reference)
|
||||
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
|
||||
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
|
||||
self.assertEqual(tx_context['amount'], self.amount)
|
||||
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
|
||||
|
||||
route_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'validation_route': False,
|
||||
'reference_prefix': tx_context['reference_prefix'],
|
||||
'landing_route': tx_context['landing_route'],
|
||||
})
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
|
||||
self.assertEqual(tx_sudo.amount, self.amount)
|
||||
self.assertEqual(tx_sudo.partner_id, self.sale_order.partner_invoice_id)
|
||||
self.assertEqual(tx_sudo.company_id, self.sale_order.company_id)
|
||||
self.assertEqual(tx_sudo.currency_id, self.sale_order.currency_id)
|
||||
self.assertEqual(tx_sudo.reference, self.reference)
|
||||
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
|
||||
|
||||
tx_sudo._set_done()
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx_sudo._finalize_post_processing()
|
||||
self.assertEqual(self.sale_order.state, 'draft') # Only a partial amount was paid
|
||||
|
||||
# Pay the remaining amount
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['sale_order_id'] = self.sale_order.id
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
|
||||
self.assertEqual(tx_context['reference_prefix'], self.reference)
|
||||
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
|
||||
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
|
||||
self.assertEqual(tx_context['amount'], self.amount)
|
||||
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
|
||||
|
||||
route_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'validation_route': False,
|
||||
'reference_prefix': tx_context['reference_prefix'],
|
||||
'landing_route': tx_context['landing_route'],
|
||||
})
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
tx2_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
self.assertEqual(tx2_sudo.sale_order_ids, self.sale_order)
|
||||
self.assertEqual(tx2_sudo.amount, self.amount)
|
||||
self.assertEqual(tx2_sudo.partner_id, self.sale_order.partner_invoice_id)
|
||||
self.assertEqual(tx2_sudo.company_id, self.sale_order.company_id)
|
||||
self.assertEqual(tx2_sudo.currency_id, self.sale_order.currency_id)
|
||||
|
||||
# We are paying a second time with the same reference (prefix)
|
||||
# a suffix is added to respect unique reference constraint
|
||||
reference = self.reference + "-1"
|
||||
self.assertEqual(tx2_sudo.reference, reference)
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
self.assertEqual(self.sale_order.transaction_ids, tx_sudo + tx2_sudo)
|
||||
|
||||
def test_13_sale_automatic_partial_payment_link_delivery(self):
|
||||
"""Test that with automatic invoice and invoicing policy based on delivered quantity, a transaction for the partial
|
||||
amount does not validate the SO."""
|
||||
# set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
# invoicing policy is based on delivered quantity
|
||||
self.product.invoice_policy = 'delivery'
|
||||
|
||||
self.amount = self.sale_order.amount_total / 2.0
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['sale_order_id'] = self.sale_order.id
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
|
||||
route_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'validation_route': False,
|
||||
'reference_prefix': tx_context['reference_prefix'],
|
||||
'landing_route': tx_context['landing_route'],
|
||||
})
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
tx_sudo._set_done()
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx_sudo._finalize_post_processing()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'draft', 'a partial transaction with automatic invoice and invoice_policy = delivery should not validate a quote')
|
||||
|
||||
def test_confirmed_transactions_comfirms_so_with_multiple_transaction(self):
|
||||
""" Test that a confirmed transaction confirms a SO even if one or more non-confirmed
|
||||
transactions are linked. """
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
self._create_transaction(
|
||||
flow='redirect',
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='draft',
|
||||
reference='Test Transaction Draft 1',
|
||||
)
|
||||
self._create_transaction(
|
||||
flow='redirect',
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='draft',
|
||||
reference='Test Transaction Draft 2',
|
||||
)
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
|
||||
def test_auto_confirm_and_auto_invoice(self):
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
|
||||
def test_auto_done_and_auto_invoice(self):
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
# Lock the sale orders when confirmed
|
||||
self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'done')
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertTrue(tx.invoice_ids.is_move_sent)
|
||||
|
||||
def test_so_partial_payment_no_invoice(self):
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total / 10.
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
self.assertFalse(tx.invoice_ids)
|
||||
self.assertFalse(self.sale_order.invoice_ids)
|
||||
|
||||
def test_already_confirmed_so_payment(self):
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
|
||||
# Confirm order before payment
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
312
odoo-bringout-oca-ocb-sale/sale/tests/test_reinvoice.py
Normal file
312
odoo-bringout-oca-ocb-sale/sale/tests/test_reinvoice.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from freezegun import freeze_time
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestReInvoice(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan',
|
||||
'company_id': cls.partner_a.company_id.id,
|
||||
})
|
||||
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA',
|
||||
'code': 'TESTSALE_REINVOICE',
|
||||
'company_id': cls.partner_a.company_id.id,
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account.id,
|
||||
'pricelist_id': cls.company_data['default_pricelist'].id,
|
||||
})
|
||||
|
||||
cls.AccountMove = cls.env['account.move'].with_context(
|
||||
default_move_type='in_invoice',
|
||||
default_invoice_date=cls.sale_order.date_order,
|
||||
mail_notrack=True,
|
||||
mail_create_nolog=True,
|
||||
)
|
||||
|
||||
def test_at_cost(self):
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
""" Test vendor bill at cost for product based on ordered and delivered quantities. """
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sale_order_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_cost'].id,
|
||||
'product_uom_qty': 2,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
sale_order_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_cost'].id,
|
||||
'product_uom_qty': 4,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# create invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_order_cost']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_delivery_cost']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice_a = move_form.save()
|
||||
invoice_a.action_post()
|
||||
|
||||
sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.company_data['product_order_cost'])
|
||||
sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.company_data['product_delivery_cost'])
|
||||
|
||||
self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product")
|
||||
self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product")
|
||||
self.assertEqual(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)")
|
||||
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 3, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 3, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
|
||||
self.assertEqual(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount")
|
||||
self.assertEqual(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount")
|
||||
|
||||
# create second invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_order_cost']
|
||||
line_form.quantity = 2.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_delivery_cost']
|
||||
line_form.quantity = 2.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice_b = move_form.save()
|
||||
invoice_b.action_post()
|
||||
|
||||
sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.company_data['product_order_cost'])
|
||||
sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.company_data['product_delivery_cost'])
|
||||
|
||||
self.assertTrue(sale_order_line5, "A new sale line should have been created with ordered product")
|
||||
self.assertTrue(sale_order_line6, "A new sale line should have been created with delivered product")
|
||||
|
||||
self.assertEqual(len(self.sale_order.order_line), 6, "There should be still 4 lines on the SO, no new created")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 4, "There should be still 2 expenses lines on the SO")
|
||||
|
||||
self.assertEqual((sale_order_line5.price_unit, sale_order_line5.qty_delivered, sale_order_line5.product_uom_qty, sale_order_line5.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 2, 0, 0), 'Sale line 5 is wrong after confirming 2e vendor invoice')
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line6.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 2, 0, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice')
|
||||
|
||||
@freeze_time('2020-01-15')
|
||||
def test_sales_team_invoiced(self):
|
||||
""" Test invoiced field from sales team ony take into account the amount the sales channel has invoiced this month """
|
||||
|
||||
invoices = self.env['account.move'].create([
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2020-01-10',
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 1000.0})],
|
||||
},
|
||||
{
|
||||
'move_type': 'out_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2020-01-10',
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 500.0})],
|
||||
},
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': '2020-01-01',
|
||||
'date': '2020-01-01',
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 800.0})],
|
||||
},
|
||||
])
|
||||
invoices.action_post()
|
||||
|
||||
for invoice in invoices:
|
||||
self.env['account.payment.register']\
|
||||
.with_context(active_model='account.move', active_ids=invoice.ids)\
|
||||
.create({})\
|
||||
._create_payments()
|
||||
|
||||
invoices.flush_model()
|
||||
self.assertRecordValues(invoices.team_id, [{'invoiced': 500.0}])
|
||||
|
||||
def test_sales_price(self):
|
||||
""" Test invoicing vendor bill at sales price for products based on delivered and ordered quantities. Check no existing SO line is incremented, but when invoicing a
|
||||
second time, increment only the delivered so line.
|
||||
"""
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sale_order_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'product_uom_qty': 2,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
sale_order_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'product_uom_qty': 3,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# create invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_delivery_sales_price']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_order_sales_price']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice_a = move_form.save()
|
||||
invoice_a.action_post()
|
||||
|
||||
sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.company_data['product_delivery_sales_price'])
|
||||
sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.company_data['product_order_sales_price'])
|
||||
|
||||
self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product")
|
||||
self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product")
|
||||
self.assertEqual(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)")
|
||||
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_delivery_sales_price'].list_price, 3, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 3, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
|
||||
self.assertEqual(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 3 should be computed by analytic amount")
|
||||
self.assertEqual(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 4 should be computed by analytic amount")
|
||||
|
||||
# create second invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_delivery_sales_price']
|
||||
line_form.quantity = 2.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_order_sales_price']
|
||||
line_form.quantity = 2.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice_b = move_form.save()
|
||||
invoice_b.action_post()
|
||||
|
||||
sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.company_data['product_delivery_sales_price'])
|
||||
sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.company_data['product_order_sales_price'])
|
||||
|
||||
self.assertFalse(sale_order_line5, "No new sale line should have been created with delivered product !!")
|
||||
self.assertTrue(sale_order_line6, "A new sale line should have been created with ordered product")
|
||||
|
||||
self.assertEqual(len(self.sale_order.order_line), 5, "There should be 5 lines on the SO, 1 new created and 1 incremented")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 3, "There should be 3 expenses lines on the SO")
|
||||
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 2, 0, 0), 'Sale line is wrong after confirming 2e vendor invoice')
|
||||
|
||||
def test_no_expense(self):
|
||||
""" Test invoicing vendor bill with no policy. Check nothing happen. """
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
# confirm SO
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_no'].id,
|
||||
'product_uom_qty': 2,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# create invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_delivery_no']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice_a = move_form.save()
|
||||
invoice_a.action_post()
|
||||
|
||||
self.assertEqual(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill")
|
||||
self.assertTrue(invoice_a.mapped('line_ids.analytic_line_ids'), "Analytic lines should be generated")
|
||||
|
||||
def test_not_reinvoicing_invoiced_so_lines(self):
|
||||
""" Test that invoiced SO lines are not re-invoiced. """
|
||||
so_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_cost'].id,
|
||||
'discount': 100.00,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
so_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'discount': 100.00,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
for line in self.sale_order.order_line:
|
||||
line.qty_delivered = 1
|
||||
# create invoice and validate it
|
||||
invoice = self.sale_order._create_invoices()
|
||||
invoice.action_post()
|
||||
|
||||
so_line3 = self.sale_order.order_line.filtered(lambda sol: sol != so_line1 and sol.product_id == self.company_data['product_delivery_cost'])
|
||||
so_line4 = self.sale_order.order_line.filtered(lambda sol: sol != so_line2 and sol.product_id == self.company_data['product_delivery_sales_price'])
|
||||
|
||||
self.assertFalse(so_line3, "No re-invoicing should have created a new sale line with product #1")
|
||||
self.assertFalse(so_line4, "No re-invoicing should have created a new sale line with product #2")
|
||||
self.assertEqual(so_line1.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 1")
|
||||
self.assertEqual(so_line2.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 2")
|
||||
|
||||
def test_not_recomputing_unit_price_for_expensed_so_lines(self):
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sol_1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_cost'].id,
|
||||
'product_uom_qty': 2,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# create invoice lines and validate it
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_a
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = self.company_data['product_order_cost']
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_distribution = {self.analytic_account.id: 100}
|
||||
invoice = move_form.save()
|
||||
invoice.action_post()
|
||||
|
||||
# update the quantity of the expensed line
|
||||
sol_2 = self.sale_order.order_line.filtered(lambda sol: sol != sol_1 and sol.product_id == self.company_data['product_order_cost'])
|
||||
|
||||
sol_2_subtotal_before = sol_2.price_unit
|
||||
sol_2.product_uom_qty = 3.0
|
||||
sol_2_subtotal_after = sol_2.price_unit
|
||||
|
||||
self.assertEqual(sol_2_subtotal_before, sol_2_subtotal_after)
|
||||
84
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_flow.py
Normal file
84
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_flow.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.sale.tests.common import TestSaleCommonBase
|
||||
|
||||
|
||||
class TestSaleFlow(TestSaleCommonBase):
|
||||
''' Test running at-install to test flows independently to other modules, e.g. 'sale_stock'. '''
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
user = cls.env['res.users'].create({
|
||||
'name': 'Because I am saleman!',
|
||||
'login': 'saleman',
|
||||
'groups_id': [(6, 0, cls.env.user.groups_id.ids), (4, cls.env.ref('account.group_account_user').id)],
|
||||
})
|
||||
user.partner_id.email = 'saleman@test.com'
|
||||
|
||||
# Shadow the current environment/cursor with the newly created user.
|
||||
cls.env = cls.env(user=user)
|
||||
cls.cr = cls.env.cr
|
||||
|
||||
cls.company = cls.env['res.company'].create({
|
||||
'name': 'Test Company',
|
||||
'currency_id': cls.env.ref('base.USD').id,
|
||||
})
|
||||
cls.company_data = cls.setup_sale_configuration_for_company(cls.company)
|
||||
|
||||
cls.partner_a = cls.env['res.partner'].create({
|
||||
'name': 'partner_a',
|
||||
'company_id': False,
|
||||
})
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan',
|
||||
'company_id': cls.company.id,
|
||||
})
|
||||
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test analytic_account',
|
||||
'code': 'analytic_account',
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
'company_id': cls.company.id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
|
||||
user.company_ids |= cls.company
|
||||
user.company_id = cls.company
|
||||
|
||||
def test_qty_delivered(self):
|
||||
''' Test 'qty_delivered' at-install to avoid a change in the behavior when 'sale_stock' is installed. '''
|
||||
|
||||
sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'analytic_account_id': self.analytic_account.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.company_data['product_order_cost'].name,
|
||||
'product_id': self.company_data['product_order_cost'].id,
|
||||
'product_uom_qty': 2,
|
||||
'qty_delivered': 1,
|
||||
'product_uom': self.company_data['product_order_cost'].uom_id.id,
|
||||
'price_unit': self.company_data['product_order_cost'].list_price,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': self.company_data['product_delivery_cost'].name,
|
||||
'product_id': self.company_data['product_delivery_cost'].id,
|
||||
'product_uom_qty': 4,
|
||||
'qty_delivered': 1,
|
||||
'product_uom': self.company_data['product_delivery_cost'].uom_id.id,
|
||||
'price_unit': self.company_data['product_delivery_cost'].list_price,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertRecordValues(sale_order.order_line, [
|
||||
{'qty_delivered': 1.0},
|
||||
{'qty_delivered': 1.0},
|
||||
])
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import HttpCase
|
||||
|
||||
|
||||
class TestOnboarding(HttpCase):
|
||||
def test_01_get_sample_sales_order_from_scratch(self):
|
||||
# Make sure there are no QO nor products
|
||||
if 'loyalty.reward' in self.env:
|
||||
self.env['loyalty.reward'].search([]).active = False
|
||||
self.env['sale.order'].search([
|
||||
('company_id', '=', self.env.company.id),
|
||||
('partner_id', '=', self.env.user.partner_id.id),
|
||||
('state', '=', 'draft')
|
||||
]).state = 'cancel'
|
||||
self.env['product.product'].search([]).active = False
|
||||
self.env.company._get_sample_sales_order()
|
||||
847
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_order.py
Normal file
847
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_order.py
Normal file
|
|
@ -0,0 +1,847 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import timedelta
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.fields import Command
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tests import tagged, Form
|
||||
from odoo.tools import float_compare
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleOrder(SaleCommon):
|
||||
|
||||
# Those tests do not rely on accounting common on purpose
|
||||
# If you need the accounting setup, use other classes (TestSaleToInvoice probably)
|
||||
|
||||
def test_computes_auto_fill(self):
|
||||
free_product, dummy_product = self.env['product.product'].create([{
|
||||
'name': 'Free product',
|
||||
'list_price': 0.0,
|
||||
}, {
|
||||
'name': 'Dummy product',
|
||||
'list_price': 0.0,
|
||||
}])
|
||||
# Test pre-computes of lines with order
|
||||
order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'display_type': 'line_section',
|
||||
'name': 'Dummy section',
|
||||
}),
|
||||
Command.create({
|
||||
'display_type': 'line_section',
|
||||
'name': 'Dummy section',
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': free_product.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': dummy_product.id,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Test pre-computes of lines creation alone
|
||||
# Ensures the creation works fine even if the computes
|
||||
# are triggered after the defaults
|
||||
order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
self.env['sale.order.line'].create([
|
||||
{
|
||||
'display_type': 'line_section',
|
||||
'name': 'Dummy section',
|
||||
'order_id': order.id,
|
||||
}, {
|
||||
'display_type': 'line_section',
|
||||
'name': 'Dummy section',
|
||||
'order_id': order.id,
|
||||
}, {
|
||||
'product_id': free_product.id,
|
||||
'order_id': order.id,
|
||||
}, {
|
||||
'product_id': dummy_product.id,
|
||||
'order_id': order.id,
|
||||
}
|
||||
])
|
||||
|
||||
def test_sale_order_standard_flow(self):
|
||||
self.assertEqual(self.sale_order.amount_total, 725.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_template(email_ctx.get('default_template_id'))
|
||||
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')
|
||||
|
||||
def test_sale_order_send_to_self(self):
|
||||
# when sender(logged in user) is also present in recipients of the mail composer,
|
||||
# user should receive mail.
|
||||
sale_order = self.env['sale.order'].with_user(self.sale_user).create({
|
||||
'partner_id': self.sale_user.partner_id.id,
|
||||
})
|
||||
email_ctx = sale_order.action_quotation_send().get('context', {})
|
||||
# We need to prevent auto mail deletion, and so we copy the template and send the mail with
|
||||
# added configuration in copied template. It will allow us to check whether mail is being
|
||||
# sent to to author or not (in case author is present in 'Recipients' of composer).
|
||||
mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')).copy({'auto_delete': False})
|
||||
# send the mail with same user as customer
|
||||
sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_template(mail_template.id)
|
||||
self.assertTrue(sale_order.state == 'sent', 'Sale : state should be changed to sent')
|
||||
mail_message = sale_order.message_ids[0]
|
||||
self.assertEqual(mail_message.author_id, sale_order.partner_id, 'Sale: author should be same as customer')
|
||||
self.assertEqual(mail_message.author_id, mail_message.partner_ids, 'Sale: author should be in composer recipients thanks to "partner_to" field set on template')
|
||||
self.assertEqual(mail_message.partner_ids, mail_message.sudo().mail_ids.recipient_ids, 'Sale: author should receive mail due to presence in composer recipients')
|
||||
|
||||
def test_sale_sequence(self):
|
||||
self.env['ir.sequence'].search([
|
||||
('code', '=', 'sale.order'),
|
||||
]).write({
|
||||
'use_date_range': True, 'prefix': 'SO/%(range_year)s/',
|
||||
})
|
||||
sale_order = self.sale_order.copy({'date_order': '2019-01-01'})
|
||||
self.assertTrue(sale_order.name.startswith('SO/2019/'))
|
||||
sale_order = self.sale_order.copy({'date_order': '2020-01-01'})
|
||||
self.assertTrue(sale_order.name.startswith('SO/2020/'))
|
||||
# In EU/BXL tz, this is actually already 01/01/2020
|
||||
sale_order = self.sale_order.with_context(tz='Europe/Brussels').copy({'date_order': '2019-12-31 23:30:00'})
|
||||
self.assertTrue(sale_order.name.startswith('SO/2020/'))
|
||||
|
||||
def test_unlink_cancel(self):
|
||||
""" Test deleting and cancelling sales orders depending on their state and on the user's rights """
|
||||
# SO in state 'draft' can be deleted
|
||||
so_copy = self.sale_order.copy()
|
||||
with self.assertRaises(AccessError):
|
||||
so_copy.with_user(self.sale_user).unlink()
|
||||
self.assertTrue(so_copy.unlink(), 'Sale: deleting a quotation should be possible')
|
||||
|
||||
# SO in state 'cancel' can be deleted
|
||||
so_copy = self.sale_order.copy()
|
||||
so_copy.action_confirm()
|
||||
self.assertTrue(so_copy.state == 'sale', 'Sale: SO should be in state "sale"')
|
||||
so_copy._action_cancel()
|
||||
self.assertTrue(so_copy.state == 'cancel', 'Sale: SO should be in state "cancel"')
|
||||
with self.assertRaises(AccessError):
|
||||
so_copy.with_user(self.sale_user).unlink()
|
||||
self.assertTrue(so_copy.unlink(), 'Sale: deleting a cancelled SO should be possible')
|
||||
|
||||
# SO in state 'sale' or 'done' cannot be deleted
|
||||
self.sale_order.action_confirm()
|
||||
self.assertTrue(self.sale_order.state == 'sale', 'Sale: SO should be in state "sale"')
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.unlink()
|
||||
|
||||
self.sale_order.action_done()
|
||||
self.assertTrue(self.sale_order.state == 'done', 'Sale: SO should be in state "done"')
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.unlink()
|
||||
|
||||
def test_compute_packaging_00(self):
|
||||
"""Create a SO and use packaging. Check we suggested suitable packaging
|
||||
according to the product_qty. Also check product_qty or product_packaging
|
||||
are correctly calculated when one of them changed.
|
||||
"""
|
||||
# Required for `product_packaging_qty` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('product.group_stock_packaging')
|
||||
packaging_single, packaging_dozen = self.env['product.packaging'].create([{
|
||||
'name': "I'm a packaging",
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
}, {
|
||||
'name': "I'm also a packaging",
|
||||
'product_id': self.product.id,
|
||||
'qty': 12.0,
|
||||
}])
|
||||
|
||||
so = self.empty_order
|
||||
so_form = Form(so)
|
||||
with so_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_uom_qty = 1.0
|
||||
so_form.save()
|
||||
self.assertEqual(so.order_line.product_packaging_id, packaging_single)
|
||||
self.assertEqual(so.order_line.product_packaging_qty, 1.0)
|
||||
with so_form.order_line.edit(0) as line:
|
||||
line.product_packaging_qty = 2.0
|
||||
so_form.save()
|
||||
self.assertEqual(so.order_line.product_uom_qty, 2.0)
|
||||
|
||||
with so_form.order_line.edit(0) as line:
|
||||
line.product_uom_qty = 24.0
|
||||
so_form.save()
|
||||
self.assertEqual(so.order_line.product_packaging_id, packaging_dozen)
|
||||
self.assertEqual(so.order_line.product_packaging_qty, 2.0)
|
||||
with so_form.order_line.edit(0) as line:
|
||||
line.product_packaging_qty = 1.0
|
||||
so_form.save()
|
||||
self.assertEqual(so.order_line.product_uom_qty, 12)
|
||||
|
||||
packaging_pack_of_10 = self.env['product.packaging'].create({
|
||||
'name': "PackOf10",
|
||||
'product_id': self.product.id,
|
||||
'qty': 10.0,
|
||||
})
|
||||
packaging_pack_of_20 = self.env['product.packaging'].create({
|
||||
'name': "PackOf20",
|
||||
'product_id': self.product.id,
|
||||
'qty': 20.0,
|
||||
})
|
||||
|
||||
so2 = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
so2_form = Form(so2)
|
||||
with so2_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_uom_qty = 10
|
||||
so2_form.save()
|
||||
self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_10.id)
|
||||
self.assertEqual(so2.order_line.product_packaging_qty, 1.0)
|
||||
|
||||
with so2_form.order_line.edit(0) as line:
|
||||
line.product_packaging_qty = 2
|
||||
so2_form.save()
|
||||
self.assertEqual(so2.order_line.product_uom_qty, 20)
|
||||
# we should have 2 pack of 10, as we've set the package_qty manually,
|
||||
# we shouldn't recompute the packaging_id, since the package_qty is protected,
|
||||
# therefor cannot be recomputed during the same transaction, which could lead
|
||||
# to an incorrect line like (qty=20,pack_qty=2,pack_id=PackOf20)
|
||||
self.assertEqual(so2.order_line.product_packaging_qty, 2)
|
||||
self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_10.id)
|
||||
|
||||
with so2_form.order_line.edit(0) as line:
|
||||
line.product_packaging_id = packaging_pack_of_20
|
||||
so2_form.save()
|
||||
self.assertEqual(so2.order_line.product_uom_qty, 20)
|
||||
# we should have 1 pack of 20, as we've set the package type manually
|
||||
self.assertEqual(so2.order_line.product_packaging_qty, 1)
|
||||
self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_20.id)
|
||||
|
||||
def test_compute_packaging_01(self):
|
||||
"""Create a SO and use packaging in a multicompany environment.
|
||||
Ensure any suggested packaging matches the SO's.
|
||||
"""
|
||||
company2 = self.env['res.company'].create([{'name': 'Company 2'}])
|
||||
generic_single_pack = self.env['product.packaging'].create({
|
||||
'name': "single pack",
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
'company_id': False,
|
||||
})
|
||||
company2_pack_of_10 = self.env['product.packaging'].create({
|
||||
'name': "pack of 10 by Company 2",
|
||||
'product_id': self.product.id,
|
||||
'qty': 10.0,
|
||||
'company_id': company2.id,
|
||||
})
|
||||
|
||||
so1 = self.empty_order
|
||||
so1_form = Form(so1)
|
||||
with so1_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_uom_qty = 10.0
|
||||
so1_form.save()
|
||||
self.assertEqual(so1.order_line.product_packaging_id, generic_single_pack)
|
||||
self.assertEqual(so1.order_line.product_packaging_qty, 10.0)
|
||||
|
||||
so2 = self.env['sale.order'].with_company(company2).create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
so2_form = Form(so2)
|
||||
with so2_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_uom_qty = 10.0
|
||||
so2_form.save()
|
||||
self.assertEqual(so2.order_line.product_packaging_id, company2_pack_of_10)
|
||||
self.assertEqual(so2.order_line.product_packaging_qty, 1.0)
|
||||
|
||||
def _create_sale_order(self):
|
||||
"""Create dummy sale order (without lines)"""
|
||||
return self.env['sale.order'].with_context(
|
||||
default_sale_order_template_id=False
|
||||
# Do not modify test behavior even if sale_management is installed
|
||||
).create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
|
||||
def test_invoicing_terms(self):
|
||||
# Enable invoicing terms
|
||||
self.env['ir.config_parameter'].sudo().set_param('account.use_invoice_terms', True)
|
||||
|
||||
# Plain invoice terms
|
||||
self.env.company.terms_type = 'plain'
|
||||
self.env.company.invoice_terms = "Coin coin"
|
||||
sale_order = self._create_sale_order()
|
||||
self.assertEqual(sale_order.note, "<p>Coin coin</p>")
|
||||
|
||||
# Html invoice terms (/terms page)
|
||||
self.env.company.terms_type = 'html'
|
||||
sale_order = self._create_sale_order()
|
||||
self.assertTrue(sale_order.note.startswith("<p>Terms & Conditions: "))
|
||||
|
||||
def test_validity_days(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.use_quotation_validity_days', True)
|
||||
self.env.company.quotation_validity_days = 5
|
||||
with freeze_time("2020-05-02"):
|
||||
sale_order = self._create_sale_order()
|
||||
|
||||
self.assertEqual(sale_order.validity_date, fields.Date.today() + timedelta(days=5))
|
||||
self.env.company.quotation_validity_days = 0
|
||||
sale_order = self._create_sale_order()
|
||||
self.assertFalse(
|
||||
sale_order.validity_date,
|
||||
"No validity date must be specified if the company validity duration is 0")
|
||||
|
||||
def test_so_names(self):
|
||||
"""Test custom context key for name_get & name_search.
|
||||
|
||||
Note: this key is used in sale_expense & sale_timesheet modules.
|
||||
"""
|
||||
SaleOrder = self.env['sale.order'].with_context(sale_show_partner_name=True)
|
||||
|
||||
res = SaleOrder.name_search(name=self.sale_order.partner_id.name)
|
||||
self.assertEqual(res[0][0], self.sale_order.id)
|
||||
|
||||
self.assertNotIn(self.sale_order.partner_id.name, self.sale_order.display_name)
|
||||
self.assertIn(
|
||||
self.sale_order.partner_id.name,
|
||||
self.sale_order.with_context(sale_show_partner_name=True).name_get()[0][1])
|
||||
|
||||
def test_state_changes(self):
|
||||
"""Test some untested state changes methods & logic."""
|
||||
self.sale_order.action_quotation_sent()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sent')
|
||||
self.assertIn(self.sale_order.partner_id, self.sale_order.message_follower_ids.partner_id)
|
||||
|
||||
self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
|
||||
self.sale_order.action_confirm()
|
||||
self.assertEqual(self.sale_order.state, 'done', "The order wasn't automatically locked at confirmation.")
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
self.sale_order.action_unlock()
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
|
||||
def test_sol_name_search(self):
|
||||
# Shouldn't raise
|
||||
self.env['sale.order']._search([('order_line', 'ilike', 'product')])
|
||||
|
||||
name_search_data = self.env['sale.order.line'].name_search(name=self.sale_order.name)
|
||||
sol_ids_found = dict(name_search_data).keys()
|
||||
self.assertEqual(list(sol_ids_found), self.sale_order.order_line.ids)
|
||||
|
||||
def test_zero_quantity(self):
|
||||
"""
|
||||
If the quantity set is 0 it should remain to 0
|
||||
Test that changing the uom do not change the quantity
|
||||
"""
|
||||
order_line = self.sale_order.order_line[0]
|
||||
order_line.product_uom_qty = 0.0
|
||||
order_line.product_uom = self.uom_dozen
|
||||
self.assertEqual(order_line.product_uom_qty, 0.0)
|
||||
|
||||
def test_discount_rounding(self):
|
||||
"""
|
||||
Check the discount is properly rounded and the price subtotal
|
||||
computed with this rounded discount
|
||||
"""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 192,
|
||||
'discount': 74.246,
|
||||
})]
|
||||
})
|
||||
self.assertEqual(sale_order.order_line.price_subtotal, 49.44, "Subtotal should be equal to 192 * (1 - 0.7425)")
|
||||
self.assertEqual(sale_order.order_line.discount, 74.25)
|
||||
|
||||
def test_tax_amount_rounding(self):
|
||||
""" Check order amounts are rounded according to settings """
|
||||
|
||||
tax_a = self.env['account.tax'].create({
|
||||
'name': 'Test tax',
|
||||
'type_tax_use': 'sale',
|
||||
'price_include': False,
|
||||
'amount_type': 'percent',
|
||||
'amount': 15.0,
|
||||
})
|
||||
|
||||
# Test Round per Line (default)
|
||||
self.env.company.tax_calculation_rounding_method = 'round_per_line'
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 6.7,
|
||||
'discount': 0,
|
||||
'tax_id': tax_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 6.7,
|
||||
'discount': 0,
|
||||
'tax_id': tax_a.ids,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertEqual(sale_order.amount_total, 15.42, "")
|
||||
|
||||
# Test Round Globally
|
||||
self.env.company.tax_calculation_rounding_method = 'round_globally'
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 6.7,
|
||||
'discount': 0,
|
||||
'tax_id': tax_a.ids,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 6.7,
|
||||
'discount': 0,
|
||||
'tax_id': tax_a.ids,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertEqual(sale_order.amount_total, 15.41, "")
|
||||
|
||||
def test_order_auto_lock_with_public_user(self):
|
||||
public_user = self.env.ref('base.public_user')
|
||||
self.sale_order.create_uid.groups_id += self.env.ref('sale.group_auto_done_setting')
|
||||
self.sale_order.with_user(public_user.id).sudo().action_confirm()
|
||||
|
||||
self.assertFalse(public_user.has_group('sale.group_auto_done_setting'))
|
||||
self.assertEqual(self.sale_order.state, 'done')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleOrderInvoicing(AccountTestInvoicingCommon, SaleCommon):
|
||||
def test_invoice_state_when_ordered_quantity_is_negative(self):
|
||||
"""When you invoice a SO line with a product that is invoiced on ordered quantities and has negative ordered quantity,
|
||||
this test ensures that the invoicing status of the SO line is 'invoiced' (and not 'upselling')."""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': -1,
|
||||
})]
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
sale_order._create_invoices(final=True)
|
||||
self.assertTrue(sale_order.invoice_status == 'invoiced', 'Sale: The invoicing status of the SO should be "invoiced"')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSalesTeam(SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# set up users
|
||||
cls.sale_team_2 = cls.env['crm.team'].create({
|
||||
'name': 'Test Sales Team (2)',
|
||||
})
|
||||
cls.user_in_team = cls.env['res.users'].create({
|
||||
'email': 'team0user@example.com',
|
||||
'login': 'team0user',
|
||||
'name': 'User in Team 0',
|
||||
})
|
||||
cls.sale_team.write({'member_ids': [4, cls.user_in_team.id]})
|
||||
cls.user_not_in_team = cls.env['res.users'].create({
|
||||
'email': 'noteamuser@example.com',
|
||||
'login': 'noteamuser',
|
||||
'name': 'User Not In Team',
|
||||
})
|
||||
|
||||
def test_assign_sales_team_from_partner_user(self):
|
||||
"""Use the team from the customer's sales person, if it is set"""
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Customer of User In Team',
|
||||
'user_id': self.user_in_team.id,
|
||||
'team_id': self.sale_team_2.id,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
self.assertEqual(sale_order.team_id.id, self.sale_team.id, 'Should assign to team of sales person')
|
||||
|
||||
def test_assign_sales_team_from_partner_team(self):
|
||||
"""If no team set on the customer's sales person, fall back to the customer's team"""
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Customer of User Not In Team',
|
||||
'user_id': self.user_not_in_team.id,
|
||||
'team_id': self.sale_team_2.id,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
self.assertEqual(sale_order.team_id.id, self.sale_team_2.id, 'Should assign to team of partner')
|
||||
|
||||
def test_assign_sales_team_when_changing_user(self):
|
||||
"""When we assign a sales person, change the team on the sales order to their team"""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'user_id': self.user_not_in_team.id,
|
||||
'partner_id': self.partner.id,
|
||||
'team_id': self.sale_team_2.id
|
||||
})
|
||||
sale_order.user_id = self.user_in_team
|
||||
self.assertEqual(sale_order.team_id.id, self.sale_team.id, 'Should assign to team of sales person')
|
||||
|
||||
def test_keep_sales_team_when_changing_user_with_no_team(self):
|
||||
"""When we assign a sales person that has no team, do not reset the team to default"""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'team_id': self.sale_team_2.id
|
||||
})
|
||||
sale_order.user_id = self.user_not_in_team
|
||||
self.assertEqual(sale_order.team_id.id, self.sale_team_2.id, 'Should not reset the team to default')
|
||||
|
||||
def test_sale_order_analytic_distribution_change(self):
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
|
||||
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
|
||||
analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account', 'plan_id': analytic_plan.id})
|
||||
analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account', 'plan_id': analytic_plan.id})
|
||||
super_product = self.env['product.product'].create({'name': 'Super Product'})
|
||||
great_product = self.env['product.product'].create({'name': 'Great Product'})
|
||||
product_no_account = self.env['product.product'].create({'name': 'Product No Account'})
|
||||
self.env['account.analytic.distribution.model'].create([
|
||||
{
|
||||
'analytic_distribution': {analytic_account_super.id: 100},
|
||||
'product_id': super_product.id,
|
||||
},
|
||||
{
|
||||
'analytic_distribution': {analytic_account_great.id: 100},
|
||||
'product_id': great_product.id,
|
||||
},
|
||||
])
|
||||
partner = self.env['res.partner'].create({'name': 'Test Partner'})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
sol = self.env['sale.order.line'].create({
|
||||
'name': super_product.name,
|
||||
'product_id': super_product.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
self.assertEqual(sol.analytic_distribution, {str(analytic_account_super.id): 100}, "The analytic distribution should be set to Super Account")
|
||||
sol.write({'product_id': great_product.id})
|
||||
self.assertEqual(sol.analytic_distribution, {str(analytic_account_great.id): 100}, "The analytic distribution should be set to Great Account")
|
||||
|
||||
so_no_analytic_account = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
sol_no_analytic_account = self.env['sale.order.line'].create({
|
||||
'name': super_product.name,
|
||||
'product_id': super_product.id,
|
||||
'order_id': so_no_analytic_account.id,
|
||||
'analytic_distribution': False,
|
||||
})
|
||||
so_no_analytic_account.action_confirm()
|
||||
self.assertFalse(sol_no_analytic_account.analytic_distribution, "The compute should not overwrite what the user has set.")
|
||||
|
||||
sale_order.action_confirm()
|
||||
sol_on_confirmed_order = self.env['sale.order.line'].create({
|
||||
'name': super_product.name,
|
||||
'product_id': super_product.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
sol_on_confirmed_order.analytic_distribution,
|
||||
{str(analytic_account_super.id): 100},
|
||||
"The analytic distribution should be set to Super Account, even for confirmed orders"
|
||||
)
|
||||
|
||||
|
||||
def test_cannot_assign_tax_of_mismatch_company(self):
|
||||
""" Test that sol cannot have assigned tax belonging to a different company from that of the sale order. """
|
||||
company_a = self.env['res.company'].create({'name': 'A'})
|
||||
company_b = self.env['res.company'].create({'name': 'B'})
|
||||
|
||||
tax_a = self.env['account.tax'].create({
|
||||
'name': 'A',
|
||||
'amount': 10,
|
||||
'company_id': company_a.id,
|
||||
})
|
||||
tax_b = self.env['account.tax'].create({
|
||||
'name': 'B',
|
||||
'amount': 10,
|
||||
'company_id': company_b.id,
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'company_id': company_a.id
|
||||
})
|
||||
product = self.env['product.product'].create({'name': 'Product'})
|
||||
|
||||
# In sudo to simulate an user that have access to both companies.
|
||||
sol = self.env['sale.order.line'].sudo().create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'order_id': sale_order.id,
|
||||
'tax_id': tax_a,
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
sol.tax_id = tax_b
|
||||
|
||||
def test_sales_team_defined_on_partner_user_no_team(self):
|
||||
""" Test that sale order picks up a team from res.partner on change if user has no team specified """
|
||||
|
||||
crm_team0 = self.env['crm.team'].create({
|
||||
'name':"Test Team A"
|
||||
})
|
||||
|
||||
crm_team1 = self.env['crm.team'].create({
|
||||
'name':"Test Team B"
|
||||
})
|
||||
|
||||
partner_a = self.env['res.partner'].create({
|
||||
'name': 'Partner A',
|
||||
'team_id': crm_team0.id,
|
||||
})
|
||||
|
||||
partner_b = self.env['res.partner'].create({
|
||||
'name': 'Partner B',
|
||||
'team_id': crm_team1.id,
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].with_user(self.user_not_in_team).create({
|
||||
'partner_id': partner_a.id,
|
||||
})
|
||||
|
||||
self.assertEqual(sale_order.team_id, crm_team0, "Sales team should change to partner's")
|
||||
sale_order.with_user(self.user_not_in_team).write({'partner_id': partner_b.id})
|
||||
self.assertEqual(sale_order.team_id, crm_team1, "Sales team should change to partner's")
|
||||
|
||||
def test_qty_delivered_on_creation(self):
|
||||
"""Checks that the qty delivered of sol is automatically set to 0.0 when an so is created"""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
})],
|
||||
})
|
||||
# As we want to determine if the value is set in the DB we need to perform a search
|
||||
self.assertFalse(self.env['sale.order.line'].search(['&', ('order_id', '=', sale_order.id), ('qty_delivered', '=', False)]))
|
||||
self.assertEqual(self.env['sale.order.line'].search(['&', ('order_id', '=', sale_order.id), ('qty_delivered', '=', 0.0)]), sale_order.order_line)
|
||||
|
||||
def test_action_recompute_taxes(self):
|
||||
'''
|
||||
This test verifies the taxes recomputation action that can be triggered
|
||||
after updating the fiscal position on a sale order document.
|
||||
'''
|
||||
special_tax = self.env['account.tax'].create({
|
||||
'name': "special_tax_10",
|
||||
'amount_type': 'percent',
|
||||
'amount': 25.0,
|
||||
'include_base_amount': True,
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
mapped_tax_a = self.env['account.tax'].create({
|
||||
'name': "tax_a",
|
||||
'amount_type': 'percent',
|
||||
'amount': 12.5,
|
||||
'include_base_amount': True,
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
mapped_tax_b = self.env['account.tax'].create({
|
||||
'name': "tax_b",
|
||||
'amount_type': 'percent',
|
||||
'amount': 5.0,
|
||||
'include_base_amount': True,
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
sales_tax = self.env['account.tax'].create({
|
||||
'name': "VAT 20%",
|
||||
'amount_type': 'percent',
|
||||
'amount': 20.0,
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
mapping_a = self.env['account.fiscal.position'].create({
|
||||
'name': 'Special Tax Reduction',
|
||||
'tax_ids': [Command.create({'tax_src_id': special_tax.id, 'tax_dest_id': mapped_tax_a.id})],
|
||||
})
|
||||
mapping_b = self.env['account.fiscal.position'].create({
|
||||
'name': 'Special Tax Reduction',
|
||||
'tax_ids': [Command.create({'tax_src_id': special_tax.id, 'tax_dest_id': mapped_tax_b.id})],
|
||||
})
|
||||
|
||||
# taxes and standard price need to be set on the product, as they will be
|
||||
# recomputed when changing the fiscal position.
|
||||
self.consumable_product.write({
|
||||
'lst_price': 300,
|
||||
'taxes_id': [Command.set((special_tax + sales_tax).ids)],
|
||||
})
|
||||
|
||||
order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.consumable_product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertEqual(order.amount_total, 300)
|
||||
self.assertEqual(order.amount_tax, 100)
|
||||
order.fiscal_position_id = mapping_a
|
||||
order._recompute_prices()
|
||||
order.action_update_taxes()
|
||||
self.assertEqual(order.amount_total, 270)
|
||||
self.assertEqual(order.amount_tax, 70)
|
||||
order.fiscal_position_id = mapping_b
|
||||
order._recompute_prices()
|
||||
order.action_update_taxes()
|
||||
self.assertEqual(order.amount_total, 252)
|
||||
self.assertEqual(order.amount_tax, 52)
|
||||
|
||||
def test_recompute_taxes_rounded_globally_multi_company_currency(self):
|
||||
'''
|
||||
Check that taxes computation are made in the currency of the company
|
||||
configured on the SO lines.
|
||||
'''
|
||||
# create a currency with no decimal
|
||||
currency_b = self.env['res.currency'].create({
|
||||
'name': 'B',
|
||||
'symbol': 'B',
|
||||
'rounding': 1.000000,
|
||||
})
|
||||
# create a company with USD as currency (rounding == 0.01)
|
||||
company_a = self.env['res.company'].create({
|
||||
'name': 'Company A',
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
})
|
||||
# create a company with currency_b as currency
|
||||
company_b = self.env['res.company'].create({
|
||||
'name': 'Company B',
|
||||
'tax_calculation_rounding_method': 'round_globally',
|
||||
'currency_id': currency_b.id,
|
||||
})
|
||||
# set company_b as default company of current user
|
||||
self.env.user.company_id = company_b
|
||||
tax_a = self.env['account.tax'].create({
|
||||
'name': 'Tax A',
|
||||
'amount': 10,
|
||||
'company_id': company_a.id,
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
})
|
||||
# create a SO from company_a
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'company_id': company_a.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 123.4,
|
||||
'tax_id': tax_a.ids,
|
||||
}),
|
||||
],
|
||||
})
|
||||
# edit the price unit
|
||||
so.write({
|
||||
'order_line': [
|
||||
Command.update(so.order_line[0].id, {'price_unit': 123.5}),
|
||||
],
|
||||
})
|
||||
# call "flush_all" as it is done in "call_kw" method to test
|
||||
# the tax values that have been recomputed after it
|
||||
self.env.flush_all()
|
||||
self.assertEqual(so.amount_tax, 12.35)
|
||||
self.assertEqual(so.amount_total, 135.85)
|
||||
# set "Rounding Method" of company A to "Round Globally"
|
||||
company_a.tax_calculation_rounding_method = 'round_globally'
|
||||
# edit the price unit
|
||||
so.write({
|
||||
'order_line': [
|
||||
Command.update(so.order_line[0].id, {'price_unit': 123.6}),
|
||||
],
|
||||
})
|
||||
self.env.flush_all()
|
||||
self.assertEqual(so.amount_tax, 12.36)
|
||||
self.assertEqual(so.amount_total, 135.96)
|
||||
|
||||
def test_recompute_taxes_rounded_globally_currency_precision(self):
|
||||
'''
|
||||
Check that taxes computation are made in the currency of the company
|
||||
configured on the SO lines.
|
||||
'''
|
||||
# create a currency with no decimal
|
||||
currency_b = self.env['res.currency'].create({
|
||||
'name': 'B',
|
||||
'symbol': 'B',
|
||||
'rounding': 1.000000,
|
||||
})
|
||||
# create a company with currency_b as currency
|
||||
company_b = self.env['res.company'].create({
|
||||
'name': 'Company B',
|
||||
'tax_calculation_rounding_method': 'round_globally',
|
||||
'currency_id': currency_b.id,
|
||||
})
|
||||
# set company_b as default company of current user
|
||||
self.env.user.company_id = company_b
|
||||
|
||||
pricelist_b = self.env['product.pricelist'].with_company(company_b).create({
|
||||
'name': 'pricelist b',
|
||||
'currency_id': self.env.ref('base.USD').id,
|
||||
})
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'Tax A',
|
||||
'amount': 19,
|
||||
'company_id': company_b.id,
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'company_id': company_b.id,
|
||||
'pricelist_id': pricelist_b.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 15.31,
|
||||
'tax_id': tax.ids,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertEqual(so.amount_tax, 2.91)
|
||||
self.assertEqual(so.amount_total, 18.22)
|
||||
self.assertEqual(so.order_line.price_tax, 2.91)
|
||||
self.assertEqual(so.order_line.price_total, 18.22)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests.common import users
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
from odoo.addons.sales_team.tests.common import TestSalesCommon
|
||||
|
||||
|
||||
class TestSaleOrderCancel(SaleCommon, TestSalesCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.template = cls.env.ref('sale.mail_template_sale_cancellation')
|
||||
cls.template.write({
|
||||
'subject': 'I can see {{ len(object.partner_id.sale_order_ids) }} order(s)',
|
||||
'body_html': 'I can see <t t-out="len(object.partner_id.sale_order_ids)"/> order(s)',
|
||||
})
|
||||
|
||||
cls.partner = cls.env['res.partner'].create({'name': 'foo'})
|
||||
|
||||
cls.manager_order, cls.salesman_order = cls.env['sale.order'].create([
|
||||
{'partner_id': cls.partner.id, 'user_id': cls.user_sales_manager.id},
|
||||
{'partner_id': cls.partner.id, 'user_id': cls.user_sales_salesman.id}
|
||||
])
|
||||
# Invalidate the cache, e.g. to clear the computation of partner.sale_order_ids
|
||||
cls.env.invalidate_all()
|
||||
|
||||
@users('user_sales_salesman')
|
||||
def test_salesman_record_rules(self):
|
||||
cancel = self.env['sale.order.cancel'].create({
|
||||
'template_id': self.template.id,
|
||||
'order_id': self.salesman_order.id,
|
||||
})
|
||||
|
||||
self.assertEqual(cancel.subject, 'I can see 1 order(s)')
|
||||
self.assertEqual(cancel.body, 'I can see 1 order(s)')
|
||||
|
||||
@users('user_sales_manager')
|
||||
def test_manager_record_rules(self):
|
||||
cancel = self.env['sale.order.cancel'].create({
|
||||
'template_id': self.template.id,
|
||||
'order_id': self.manager_order.id,
|
||||
})
|
||||
|
||||
self.assertEqual(cancel.subject, 'I can see 2 order(s)')
|
||||
self.assertEqual(cancel.body, 'I can see 2 order(s)')
|
||||
985
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_prices.py
Normal file
985
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_prices.py
Normal file
|
|
@ -0,0 +1,985 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from freezegun import freeze_time
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tools import float_compare, mute_logger, float_round
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSalePrices(SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.discount = 10 # %
|
||||
|
||||
# Needed when run without demo data
|
||||
# s.t. taxes creation doesn't fail
|
||||
cls.env.company.account_fiscal_country_id = cls.env.ref('base.be')
|
||||
|
||||
def _create_discount_pricelist_rule(self, **additional_values):
|
||||
return self.env['product.pricelist.item'].create({
|
||||
'pricelist_id': self.pricelist.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': self.discount,
|
||||
**additional_values,
|
||||
})
|
||||
|
||||
def test_pricelist_minimal_qty(self):
|
||||
""" Verify the quantity and uom are correctly provided to the pricelist API"""
|
||||
pricelist_rule = self._create_discount_pricelist_rule(
|
||||
min_quantity=4.0,
|
||||
)
|
||||
product_price = self.product.lst_price
|
||||
product_dozen_price = product_price * 12
|
||||
discount = 1 - self.discount/100
|
||||
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 3.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 4.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 0.4,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 0.3,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
})
|
||||
]
|
||||
|
||||
discounted_lines = self.empty_order.order_line.filtered('pricelist_item_id')
|
||||
self.assertEqual(discounted_lines, self.empty_order.order_line[1:5])
|
||||
self.assertEqual(discounted_lines.pricelist_item_id, pricelist_rule)
|
||||
self.assertTrue(all(not line.discount for line in self.empty_order.order_line))
|
||||
self.assertEqual(
|
||||
discounted_lines.mapped('price_unit'),
|
||||
[
|
||||
product_price*discount,
|
||||
product_price*discount,
|
||||
product_dozen_price*discount,
|
||||
product_dozen_price*discount
|
||||
]
|
||||
)
|
||||
|
||||
self.pricelist.discount_policy = 'without_discount'
|
||||
self.empty_order._recompute_prices()
|
||||
self.assertEqual(
|
||||
discounted_lines.mapped('price_unit'),
|
||||
[product_price, product_price, product_dozen_price, product_dozen_price])
|
||||
self.assertEqual(discounted_lines.mapped('discount'), [self.discount]*len(discounted_lines))
|
||||
|
||||
def test_pricelist_dates(self):
|
||||
""" Verify the order date is correctly provided to the pricelist API"""
|
||||
today = fields.Datetime.today()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
|
||||
pricelist_rule = self._create_discount_pricelist_rule(
|
||||
date_start=today - timedelta(hours=1),
|
||||
date_end=today + timedelta(hours=23),
|
||||
)
|
||||
|
||||
with freeze_time(today):
|
||||
# Create an order today, add line today, rule active today works
|
||||
self.empty_order.date_order = today
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
self.assertEqual(order_line.pricelist_item_id, pricelist_rule)
|
||||
self.assertEqual(
|
||||
order_line.price_unit,
|
||||
self.product.lst_price * (1 - self.discount / 100.0))
|
||||
self.assertEqual(order_line.discount, 0.0)
|
||||
|
||||
# Create an order tomorrow, add line today, rule active today doesn't work
|
||||
self.empty_order.date_order = tomorrow
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
self.assertFalse(order_line.pricelist_item_id)
|
||||
self.assertEqual(order_line.price_unit, self.product.lst_price)
|
||||
self.assertEqual(order_line.discount, 0.0)
|
||||
|
||||
with freeze_time(tomorrow):
|
||||
# Create an order tomorrow, add line tomorrow, rule active today doesn't work
|
||||
self.empty_order.date_order = tomorrow
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
self.assertFalse(order_line.pricelist_item_id)
|
||||
self.assertEqual(order_line.price_unit, self.product.lst_price)
|
||||
self.assertEqual(order_line.discount, 0.0)
|
||||
|
||||
# Create an order today, add line tomorrow, rule active today works
|
||||
self.empty_order.date_order = today
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
self.assertEqual(order_line.pricelist_item_id, pricelist_rule)
|
||||
self.assertEqual(
|
||||
order_line.price_unit,
|
||||
self.product.lst_price * (1 - self.discount / 100.0))
|
||||
self.assertEqual(order_line.discount, 0.0)
|
||||
|
||||
self.assertEqual(
|
||||
self.empty_order.amount_untaxed,
|
||||
self.product.lst_price * 3.8) # Discount of 10% on 2 of the 4 sol
|
||||
|
||||
def test_pricelist_product_context(self):
|
||||
""" Verify that the product attributes extra prices are correctly considered """
|
||||
no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': 'No Variant Test Attribute',
|
||||
'create_variant': 'no_variant',
|
||||
'value_ids': [
|
||||
Command.create({'name': 'A'}),
|
||||
Command.create({'name': 'B'}),
|
||||
Command.create({'name': 'C'}),
|
||||
],
|
||||
})
|
||||
product_template = self.env['product.template'].create({
|
||||
'name': 'Test Template with no_variant attributes',
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': no_variant_attribute.id,
|
||||
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
|
||||
}),
|
||||
],
|
||||
'list_price': 75.0,
|
||||
'taxes_id': False,
|
||||
})
|
||||
|
||||
# Specify an extra_price on a variant
|
||||
ptavs = product_template.attribute_line_ids.product_template_value_ids
|
||||
ptavs[0].price_extra = 5.0
|
||||
ptavs[2].price_extra = 25.0
|
||||
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product_template.product_variant_id.id,
|
||||
'product_no_variant_attribute_value_ids': [Command.link(ptav.id)]
|
||||
})
|
||||
for ptav in ptavs
|
||||
]
|
||||
|
||||
order_lines = self.empty_order.order_line
|
||||
self.assertEqual(order_lines[0].price_unit, 80.0)
|
||||
self.assertEqual(order_lines[1].price_unit, 75.0)
|
||||
self.assertEqual(order_lines[2].price_unit, 100.0)
|
||||
|
||||
def test_no_pricelist_rules(self):
|
||||
"""Check currencies and uom conversions when no pricelist rule is available"""
|
||||
# UoM Conversion
|
||||
# Selling dozens => price_unit = 12*price by unit
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 2.0,
|
||||
}),
|
||||
]
|
||||
self.assertEqual(self.empty_order.order_line.price_unit, 240.0)
|
||||
|
||||
other_currency = self._enable_currency('EUR')
|
||||
pricelist_in_other_curr = self.env['product.pricelist'].create({
|
||||
'name': 'Test Pricelist (EUR)',
|
||||
'currency_id': other_currency.id,
|
||||
})
|
||||
with freeze_time('2022-08-19'):
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': fields.Date.today(),
|
||||
'rate': 1.0,
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
order_in_other_currency = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'pricelist_id': pricelist_in_other_curr.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 2.0,
|
||||
}),
|
||||
]
|
||||
})
|
||||
self.assertEqual(order_in_other_currency.amount_total, 480.0)
|
||||
|
||||
def test_negative_discounts(self):
|
||||
"""aka surcharges"""
|
||||
self.discount = -10
|
||||
rule = self._create_discount_pricelist_rule()
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
self.assertEqual(order_line.price_unit, 22.0)
|
||||
self.assertEqual(order_line.pricelist_item_id, rule)
|
||||
|
||||
# Even when the discount is supposed to be shown
|
||||
# Surcharges shouldn't be shown to the user
|
||||
self.pricelist.discount_policy = 'without_discount'
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
self.assertEqual(order_line.price_unit, 22.0)
|
||||
self.assertEqual(order_line.pricelist_item_id, rule)
|
||||
|
||||
def test_pricelist_based_on_another(self):
|
||||
""" Test price and discount are correctly applied with a pricelist based on an other one"""
|
||||
self.product.lst_price = 100
|
||||
|
||||
base_pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'First pricelist',
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'compute_price': 'percentage',
|
||||
'base': 'list_price',
|
||||
'percent_price': 10,
|
||||
'applied_on': '3_global',
|
||||
'name': 'First discount',
|
||||
})],
|
||||
})
|
||||
|
||||
self.pricelist.write({
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'compute_price': 'formula',
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': base_pricelist.id,
|
||||
'price_discount': 10,
|
||||
'applied_on': '3_global',
|
||||
'name': 'Second discount',
|
||||
})],
|
||||
})
|
||||
|
||||
self.empty_order.write({
|
||||
'date_order': '2018-07-11',
|
||||
})
|
||||
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
self.assertEqual(order_line.pricelist_item_id, self.pricelist.item_ids)
|
||||
self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied")
|
||||
self.assertEqual(order_line.discount, 19, "Second discount not applied")
|
||||
|
||||
def test_pricelist_with_another_currency(self):
|
||||
""" Test prices are correctly applied with a pricelist with another currency"""
|
||||
self.product.lst_price = 100
|
||||
|
||||
currency_eur = self._enable_currency('EUR')
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2018-07-11',
|
||||
'rate': 2.0,
|
||||
'currency_id': currency_eur.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
with mute_logger('odoo.models.unlink'):
|
||||
self.env['res.currency.rate'].search(
|
||||
[('currency_id', '=', self.env.company.currency_id.id)]
|
||||
).unlink()
|
||||
new_uom = self.env['uom.uom'].create({
|
||||
'name': '10 units',
|
||||
'factor_inv': 10,
|
||||
'uom_type': 'bigger',
|
||||
'rounding': 1.0,
|
||||
'category_id': self.uom_unit.category_id.id,
|
||||
})
|
||||
|
||||
# This pricelist doesn't show the discount
|
||||
pricelist_eur = self.env['product.pricelist'].create({
|
||||
'name': 'First pricelist',
|
||||
'currency_id': currency_eur.id,
|
||||
'discount_policy': 'with_discount',
|
||||
'item_ids': [Command.create({
|
||||
'compute_price': 'percentage',
|
||||
'base': 'list_price',
|
||||
'percent_price': 10,
|
||||
'applied_on': '3_global',
|
||||
'name': 'First discount'
|
||||
})],
|
||||
})
|
||||
|
||||
self.empty_order.write({
|
||||
'date_order': '2018-07-12',
|
||||
'pricelist_id': pricelist_eur.id,
|
||||
})
|
||||
|
||||
order_line = self.env['sale.order.line'].create({
|
||||
'order_id': self.empty_order.id,
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
|
||||
# force compute uom and prices
|
||||
self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied")
|
||||
order_line.product_uom = new_uom
|
||||
self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied")
|
||||
|
||||
def test_multi_currency_discount(self):
|
||||
"""Verify the currency used for pricelist price & discount computation."""
|
||||
product_1 = self.consumable_product
|
||||
product_2 = self.service_product
|
||||
|
||||
# Make sure the company is in USD
|
||||
main_company = self.env.ref('base.main_company')
|
||||
main_curr = main_company.currency_id
|
||||
current_curr = self.env.company.currency_id # USD
|
||||
other_curr = self._enable_currency('EUR')
|
||||
# main_company.currency_id = other_curr # product.currency_id when no company_id set
|
||||
other_company = self.env['res.company'].create({
|
||||
'name': 'Test',
|
||||
'currency_id': other_curr.id
|
||||
})
|
||||
user_in_other_company = self.env['res.users'].create({
|
||||
'company_id': other_company.id,
|
||||
'company_ids': [Command.set([other_company.id])],
|
||||
'name': 'E.T',
|
||||
'login': 'hohoho',
|
||||
})
|
||||
with mute_logger('odoo.models.unlink'):
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': '2010-01-01',
|
||||
'rate': 2.0,
|
||||
'currency_id': main_curr.id,
|
||||
'company_id': False,
|
||||
})
|
||||
|
||||
product_1.company_id = False
|
||||
product_2.company_id = False
|
||||
|
||||
self.assertEqual(product_1.currency_id, main_curr)
|
||||
self.assertEqual(product_2.currency_id, main_curr)
|
||||
self.assertEqual(product_1.cost_currency_id, current_curr)
|
||||
self.assertEqual(product_2.cost_currency_id, current_curr)
|
||||
|
||||
product_1_ctxt = product_1.with_user(user_in_other_company)
|
||||
product_2_ctxt = product_2.with_user(user_in_other_company)
|
||||
self.assertEqual(product_1_ctxt.currency_id, main_curr)
|
||||
self.assertEqual(product_2_ctxt.currency_id, main_curr)
|
||||
self.assertEqual(product_1_ctxt.cost_currency_id, other_curr)
|
||||
self.assertEqual(product_2_ctxt.cost_currency_id, other_curr)
|
||||
|
||||
product_1.lst_price = 100.0
|
||||
product_2_ctxt.standard_price = 10.0 # cost is company_dependent
|
||||
|
||||
pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'Test multi-currency',
|
||||
'discount_policy': 'without_discount',
|
||||
'currency_id': other_curr.id,
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
'base': 'list_price',
|
||||
'product_id': product_1.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 20,
|
||||
}),
|
||||
Command.create({
|
||||
'base': 'standard_price',
|
||||
'product_id': product_2.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 10,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Create a SO in the other company
|
||||
##################################
|
||||
# product_currency = main_company.currency_id when no company_id on the product
|
||||
|
||||
# CASE 1:
|
||||
# company currency = so currency
|
||||
# product_1.currency != so currency
|
||||
# product_2.cost_currency_id = so currency
|
||||
sales_order = product_1_ctxt.with_context(mail_notrack=True, mail_create_nolog=True).env['sale.order'].create({
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': product_1.id,
|
||||
'product_uom_qty': 1.0
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': product_2.id,
|
||||
'product_uom_qty': 1.0
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
so_line_1 = sales_order.order_line[0]
|
||||
so_line_2 = sales_order.order_line[1]
|
||||
self.assertEqual(so_line_1.discount, 20)
|
||||
self.assertEqual(so_line_1.price_unit, 50.0)
|
||||
self.assertEqual(so_line_2.discount, 10)
|
||||
self.assertEqual(so_line_2.price_unit, 10)
|
||||
|
||||
# CASE 2
|
||||
# company currency != so currency
|
||||
# product_1.currency == so currency
|
||||
# product_2.cost_currency_id != so currency
|
||||
pricelist.currency_id = main_curr
|
||||
sales_order = product_1_ctxt.with_context(mail_notrack=True, mail_create_nolog=True).env['sale.order'].create({
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'order_line': [
|
||||
# Verify discount is considered in create hack
|
||||
Command.create({
|
||||
'product_id': product_1.id,
|
||||
'product_uom_qty': 1.0
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': product_2.id,
|
||||
'product_uom_qty': 1.0
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
so_line_1 = sales_order.order_line[0]
|
||||
so_line_2 = sales_order.order_line[1]
|
||||
self.assertEqual(so_line_1.discount, 20)
|
||||
self.assertEqual(so_line_1.price_unit, 100.0)
|
||||
self.assertEqual(so_line_2.discount, 10)
|
||||
self.assertEqual(so_line_2.price_unit, 20)
|
||||
|
||||
def test_update_prices(self):
|
||||
"""Test prices recomputation on SO's.
|
||||
|
||||
`_recompute_prices` is shown as a button to update
|
||||
prices when the pricelist was changed.
|
||||
"""
|
||||
sale_order = self.sale_order
|
||||
so_amount = sale_order.amount_total
|
||||
sale_order._recompute_prices()
|
||||
self.assertEqual(
|
||||
sale_order.amount_total, so_amount,
|
||||
"Updating the prices of an unmodified SO shouldn't modify the amounts")
|
||||
|
||||
pricelist = sale_order.pricelist_id
|
||||
pricelist.item_ids = [
|
||||
fields.Command.create({
|
||||
'percent_price': 5.0,
|
||||
'compute_price': 'percentage'
|
||||
})
|
||||
]
|
||||
pricelist.discount_policy = "without_discount"
|
||||
sale_order._recompute_prices()
|
||||
|
||||
self.assertTrue(all(line.discount == 5 for line in sale_order.order_line))
|
||||
self.assertEqual(sale_order.amount_undiscounted, so_amount)
|
||||
self.assertEqual(sale_order.amount_total, 0.95*so_amount)
|
||||
|
||||
pricelist.discount_policy = "with_discount"
|
||||
sale_order._recompute_prices()
|
||||
|
||||
self.assertTrue(all(line.discount == 0 for line in sale_order.order_line))
|
||||
self.assertEqual(sale_order.amount_undiscounted, so_amount)
|
||||
self.assertEqual(sale_order.amount_total, 0.95*so_amount)
|
||||
|
||||
# Taxes tests:
|
||||
# We do not rely on accounting common on purpose to avoid
|
||||
# all the useless setup not needed here.
|
||||
# If you need the accounting common (journals, ...), use/make another test class
|
||||
|
||||
def test_sale_tax_mapping(self):
|
||||
tax_a, tax_b = self.env['account.tax'].create([{
|
||||
'name': 'Test tax A',
|
||||
'type_tax_use': 'sale',
|
||||
'price_include': True,
|
||||
'amount': 15.0,
|
||||
}, {
|
||||
'name': 'Test tax B',
|
||||
'type_tax_use': 'sale',
|
||||
'amount': 6.0,
|
||||
}])
|
||||
|
||||
country_belgium = self.env['res.country'].search([
|
||||
('name', '=', 'Belgium'),
|
||||
], limit=1)
|
||||
fiscal_pos = self.env['account.fiscal.position'].create({
|
||||
'name': 'Test Fiscal Position',
|
||||
'auto_apply': True,
|
||||
'country_id': country_belgium.id,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_a.id,
|
||||
'tax_dest_id': tax_b.id
|
||||
})]
|
||||
})
|
||||
|
||||
# setting up partner:
|
||||
self.partner.country_id = country_belgium
|
||||
|
||||
self.product.write({
|
||||
'lst_price': 115,
|
||||
'taxes_id': [Command.set(tax_a.ids)]
|
||||
})
|
||||
|
||||
self.pricelist.write({
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'applied_on': '3_global',
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 54,
|
||||
})]
|
||||
})
|
||||
|
||||
# creating SO
|
||||
self.empty_order.write({
|
||||
'fiscal_position_id': fiscal_pos.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product.id,
|
||||
})],
|
||||
})
|
||||
|
||||
# Update Prices
|
||||
self.empty_order._recompute_prices()
|
||||
|
||||
# Check that the discount displayed is the correct one
|
||||
self.assertEqual(
|
||||
self.empty_order.order_line.discount, 54,
|
||||
"Wrong discount computed for specified product & pricelist"
|
||||
)
|
||||
# Additional to check for overall consistency
|
||||
self.assertEqual(
|
||||
self.empty_order.order_line.price_unit, 100,
|
||||
"Wrong unit price computed for specified product & pricelist"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.empty_order.order_line.price_subtotal, 46,
|
||||
"Wrong subtotal price computed for specified product & pricelist"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.empty_order.order_line.tax_id.id, tax_b.id,
|
||||
"Wrong tax applied for specified product & pricelist"
|
||||
)
|
||||
|
||||
def test_fiscalposition_application(self):
|
||||
"""Test application of a fiscal position mapping
|
||||
price included to price included tax
|
||||
"""
|
||||
# If test is run without demo data
|
||||
# pricelists are not automatically enabled
|
||||
self._enable_pricelists()
|
||||
pricelist = self.pricelist
|
||||
partner = self.partner
|
||||
|
||||
(
|
||||
tax_fixed_incl,
|
||||
tax_fixed_excl,
|
||||
tax_include_src,
|
||||
tax_include_dst,
|
||||
tax_exclude_src,
|
||||
tax_exclude_dst,
|
||||
) = self.env['account.tax'].create([{
|
||||
'name': "fixed include",
|
||||
'amount': '10.00',
|
||||
'amount_type': 'fixed',
|
||||
'price_include': True,
|
||||
}, {
|
||||
'name': "fixed exclude",
|
||||
'amount': '10.00',
|
||||
'amount_type': 'fixed',
|
||||
'price_include': False,
|
||||
}, {
|
||||
'name': "Include 21%",
|
||||
'amount': 21.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': True,
|
||||
}, {
|
||||
'name': "Include 6%",
|
||||
'amount': 6.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': True,
|
||||
}, {
|
||||
'name': "Exclude 15%",
|
||||
'amount': 15.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': False,
|
||||
}, {
|
||||
'name': "Exclude 21%",
|
||||
'amount': 21.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': False,
|
||||
}])
|
||||
|
||||
(
|
||||
product_tmpl_a,
|
||||
product_tmpl_b,
|
||||
product_tmpl_c,
|
||||
product_tmpl_d,
|
||||
) = self.env['product.template'].create([{
|
||||
'name': "Voiture",
|
||||
'list_price': 121,
|
||||
'taxes_id': [Command.set([tax_include_src.id])]
|
||||
}, {
|
||||
'name': "Voiture",
|
||||
'list_price': 100,
|
||||
'taxes_id': [Command.set([tax_exclude_src.id])]
|
||||
}, {
|
||||
'name': "Voiture",
|
||||
'list_price': 100,
|
||||
'taxes_id': [Command.set([tax_fixed_incl.id, tax_exclude_src.id])]
|
||||
}, {
|
||||
'name': "Voiture",
|
||||
'list_price': 100,
|
||||
'taxes_id': [Command.set([tax_fixed_excl.id, tax_include_src.id])]
|
||||
}])
|
||||
|
||||
(
|
||||
fpos_incl_incl,
|
||||
fpos_excl_incl,
|
||||
fpos_incl_excl,
|
||||
fpos_excl_excl,
|
||||
) = self.env['account.fiscal.position'].create([{
|
||||
'name': "incl -> incl",
|
||||
'sequence': 1,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_include_src.id,
|
||||
'tax_dest_id': tax_include_dst.id,
|
||||
})]
|
||||
}, {
|
||||
'name': "excl -> incl",
|
||||
'sequence': 2,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_exclude_src.id,
|
||||
'tax_dest_id': tax_include_dst.id,
|
||||
})]
|
||||
}, {
|
||||
'name': "incl -> excl",
|
||||
'sequence': 3,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_include_src.id,
|
||||
'tax_dest_id': tax_exclude_dst.id,
|
||||
})]
|
||||
}, {
|
||||
'name': "excl -> excp",
|
||||
'sequence': 4,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_exclude_src.id,
|
||||
'tax_dest_id': tax_exclude_dst.id,
|
||||
})]
|
||||
}])
|
||||
|
||||
# Create the SO with one SO line and apply a pricelist and fiscal position on it
|
||||
# Then check if price unit and price subtotal matches the expected values
|
||||
|
||||
SaleOrder = self.env['sale.order']
|
||||
|
||||
# Test Mapping included to included
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_incl_incl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_a.product_variant_id.name
|
||||
line.product_id = product_tmpl_a.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 106, 'price_subtotal': 100}])
|
||||
|
||||
# Test Mapping excluded to included
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_excl_incl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_b.product_variant_id.name
|
||||
line.product_id = product_tmpl_b.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 94.34}])
|
||||
|
||||
# Test Mapping included to excluded
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_incl_excl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_a.product_variant_id.name
|
||||
line.product_id = product_tmpl_a.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
||||
|
||||
# Test Mapping excluded to excluded
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_excl_excl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_b.product_variant_id.name
|
||||
line.product_id = product_tmpl_b.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
||||
|
||||
# Test Mapping (included,excluded) to (included, included)
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_excl_incl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_c.product_variant_id.name
|
||||
line.product_id = product_tmpl_c.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 84.91}])
|
||||
|
||||
# Test Mapping (excluded,included) to (excluded, excluded)
|
||||
order_form = Form(SaleOrder)
|
||||
order_form.partner_id = partner
|
||||
order_form.pricelist_id = pricelist
|
||||
order_form.fiscal_position_id = fpos_incl_excl
|
||||
with order_form.order_line.new() as line:
|
||||
line.name = product_tmpl_d.product_variant_id.name
|
||||
line.product_id = product_tmpl_d.product_variant_id
|
||||
line.product_uom_qty = 1.0
|
||||
sale_order = order_form.save()
|
||||
self.assertRecordValues(sale_order.order_line, [{'price_unit': 100, 'price_subtotal': 100}])
|
||||
|
||||
def test_so_tax_mapping(self):
|
||||
order = self.empty_order
|
||||
|
||||
tax_include, tax_exclude = self.env['account.tax'].create([{
|
||||
'name': 'Include Tax',
|
||||
'amount': '21.00',
|
||||
'price_include': True,
|
||||
'type_tax_use': 'sale',
|
||||
}, {
|
||||
'name': 'Exclude Tax',
|
||||
'amount': '0.00',
|
||||
'type_tax_use': 'sale',
|
||||
}])
|
||||
|
||||
self.product.write({
|
||||
'list_price': 121,
|
||||
'taxes_id': [Command.set(tax_include.ids)]
|
||||
})
|
||||
|
||||
fpos = self.env['account.fiscal.position'].create({
|
||||
'name': 'Test Fiscal Position',
|
||||
'sequence': 1,
|
||||
'tax_ids': [Command.create({
|
||||
'tax_src_id': tax_include.id,
|
||||
'tax_dest_id': tax_exclude.id,
|
||||
})],
|
||||
})
|
||||
|
||||
order.write({
|
||||
'fiscal_position_id': fpos.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product.id,
|
||||
})]
|
||||
})
|
||||
|
||||
# Check the unit price of SO line
|
||||
self.assertEqual(
|
||||
100, order.order_line[0].price_unit,
|
||||
"The included tax must be subtracted to the price")
|
||||
|
||||
def test_free_product_and_price_include_fixed_tax(self):
|
||||
""" Check that fixed tax include are correctly computed while the price_unit is 0 """
|
||||
taxes = self.env['account.tax'].create([{
|
||||
'name': 'BEBAT 0.05',
|
||||
'type_tax_use': 'sale',
|
||||
'amount_type': 'fixed',
|
||||
'amount': 0.05,
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
}, {
|
||||
'name': 'Recupel 0.25',
|
||||
'type_tax_use': 'sale',
|
||||
'amount_type': 'fixed',
|
||||
'amount': 0.25,
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
}])
|
||||
order = self.empty_order
|
||||
order.order_line = [Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 0.0,
|
||||
'tax_id': [
|
||||
Command.set(taxes.ids),
|
||||
],
|
||||
})]
|
||||
|
||||
self.assertRecordValues(order.order_line, [{
|
||||
'price_tax': 0.3,
|
||||
'price_subtotal': -0.3,
|
||||
'price_total': 0.0,
|
||||
}])
|
||||
self.assertRecordValues(order, [{
|
||||
'amount_untaxed': -0.30,
|
||||
'amount_tax': 0.30,
|
||||
'amount_total': 0.0,
|
||||
}])
|
||||
|
||||
def test_sale_with_taxes(self):
|
||||
""" Test SO with taxes applied on its lines and check subtotal applied on its lines and total applied on the SO """
|
||||
tax_include, tax_exclude = self.env['account.tax'].create([{
|
||||
'name': 'Tax with price include',
|
||||
'amount': 10,
|
||||
'price_include': True
|
||||
}, {
|
||||
'name': 'Tax with no price include',
|
||||
'amount': 10,
|
||||
}])
|
||||
|
||||
# Apply taxes on the sale order lines
|
||||
self.sale_order.order_line[0].write({'tax_id': [Command.link(tax_include.id)]})
|
||||
self.sale_order.order_line[1].write({'tax_id': [Command.link(tax_exclude.id)]})
|
||||
|
||||
for line in self.sale_order.order_line:
|
||||
if line.tax_id.price_include:
|
||||
price = line.price_unit * line.product_uom_qty - line.price_tax
|
||||
else:
|
||||
price = line.price_unit * line.product_uom_qty
|
||||
|
||||
self.assertEqual(float_compare(line.price_subtotal, price, precision_digits=2), 0)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
self.sale_order.amount_total,
|
||||
self.sale_order.amount_untaxed + self.sale_order.amount_tax,
|
||||
places=2)
|
||||
|
||||
def test_discount_and_untaxed_subtotal(self):
|
||||
"""When adding a discount on a SO line, this test ensures that the untaxed amount to invoice is
|
||||
equal to the untaxed subtotal"""
|
||||
self.product.invoice_policy = 'delivery'
|
||||
order = self.empty_order
|
||||
|
||||
order.order_line = [Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 38,
|
||||
'price_unit': 541.26,
|
||||
'discount': 2.00,
|
||||
})]
|
||||
order.action_confirm()
|
||||
line = order.order_line
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 0)
|
||||
|
||||
line.qty_delivered = 38
|
||||
# (541.26 - 0.02 * 541.26) * 38 = 20156.5224 ~= 20156.52
|
||||
self.assertEqual(line.price_subtotal, 20156.52)
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
|
||||
|
||||
# Same with an included-in-price tax
|
||||
order = order.copy()
|
||||
line = order.order_line
|
||||
line.tax_id = [Command.create({
|
||||
'name': 'Super Tax',
|
||||
'amount_type': 'percent',
|
||||
'amount': 15.0,
|
||||
'price_include': True,
|
||||
})]
|
||||
order.action_confirm()
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 0)
|
||||
|
||||
line.qty_delivered = 38
|
||||
# (541,26 / 1,15) * ,98 * 38 = 17527,410782609 ~= 17527.41
|
||||
self.assertEqual(line.price_subtotal, 17527.41)
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
|
||||
|
||||
def test_discount_and_amount_undiscounted(self):
|
||||
"""When adding a discount on a SO line, this test ensures that amount undiscounted is
|
||||
consistent with the used tax"""
|
||||
order = self.empty_order
|
||||
|
||||
order.order_line = [Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 100.0,
|
||||
'discount': 1.00,
|
||||
})]
|
||||
order.action_confirm()
|
||||
order_line = order.order_line
|
||||
|
||||
# test discount and qty 1
|
||||
self.assertEqual(order.amount_undiscounted, 100.0)
|
||||
self.assertEqual(order_line.price_subtotal, 99.0)
|
||||
|
||||
# more quantity 1 -> 3
|
||||
order_line.write({
|
||||
'product_uom_qty': 3.0,
|
||||
'price_unit': 100.0,
|
||||
})
|
||||
order.invalidate_recordset(['amount_undiscounted'])
|
||||
|
||||
self.assertEqual(order.amount_undiscounted, 300.0)
|
||||
self.assertEqual(order_line.price_subtotal, 297.0)
|
||||
|
||||
# undiscounted
|
||||
order_line.discount = 0.0
|
||||
self.assertEqual(order_line.price_subtotal, 300.0)
|
||||
self.assertEqual(order.amount_undiscounted, 300.0)
|
||||
|
||||
# Same with an included-in-price tax
|
||||
order = order.copy()
|
||||
line = order.order_line
|
||||
line.tax_id = [Command.create({
|
||||
'name': 'Super Tax',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'price_include': True,
|
||||
})]
|
||||
line.discount = 50.0
|
||||
order.action_confirm()
|
||||
|
||||
# 300 with 10% incl tax -> 272.72 total tax excluded without discount
|
||||
# 136.36 price tax excluded with discount applied
|
||||
self.assertEqual(order.amount_undiscounted, 272.72)
|
||||
self.assertEqual(line.price_subtotal, 136.36)
|
||||
|
||||
def test_product_quantity_rounding(self):
|
||||
"""When adding a sale order line, product quantity should be rounded
|
||||
according to decimal precision"""
|
||||
order = self.empty_order
|
||||
|
||||
product_uom_qty = 0.333333
|
||||
order.order_line = [Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': product_uom_qty,
|
||||
'price_unit': 75.0,
|
||||
})]
|
||||
order.action_confirm()
|
||||
line = order.order_line
|
||||
quantity_precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
expected_price_subtotal = line.price_unit * float_round(product_uom_qty, precision_digits=quantity_precision)
|
||||
self.assertAlmostEqual(line.price_subtotal, expected_price_subtotal)
|
||||
self.assertEqual(order.amount_total, order.tax_totals.get('amount_total'))
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
# -*- 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 tagged
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.product.tests.test_product_attribute_value_config import TestProductAttributeValueCommon
|
||||
from odoo.addons.product.tests.common import ProductAttributesCommon
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestSaleProductAttributeValueCommon(AccountTestInvoicingCommon, TestProductAttributeValueCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
cls.computer.company_id = cls.env.company
|
||||
cls.computer = cls.computer.with_env(cls.env)
|
||||
|
||||
@classmethod
|
||||
def _setup_currency(cls, currency_ratio=2):
|
||||
"""Get or create a currency. This makes the test non-reliant on demo.
|
||||
|
||||
With an easy currency rate, for a simple 2 ratio in the following tests.
|
||||
"""
|
||||
from_currency = cls.computer.currency_id
|
||||
cls._set_or_create_rate_today(from_currency, rate=1)
|
||||
|
||||
to_currency = cls._get_or_create_currency("my currency", "C")
|
||||
cls._set_or_create_rate_today(to_currency, currency_ratio)
|
||||
return to_currency
|
||||
|
||||
@classmethod
|
||||
def _set_or_create_rate_today(cls, currency, rate):
|
||||
"""Get or create a currency rate for today. This makes the test
|
||||
non-reliant on demo data."""
|
||||
name = fields.Date.today()
|
||||
currency_id = currency.id
|
||||
company_id = cls.env.company.id
|
||||
|
||||
CurrencyRate = cls.env['res.currency.rate']
|
||||
|
||||
currency_rate = CurrencyRate.search([
|
||||
('company_id', '=', company_id),
|
||||
('currency_id', '=', currency_id),
|
||||
('name', '=', name),
|
||||
])
|
||||
|
||||
if currency_rate:
|
||||
currency_rate.rate = rate
|
||||
else:
|
||||
CurrencyRate.create({
|
||||
'company_id': company_id,
|
||||
'currency_id': currency_id,
|
||||
'name': name,
|
||||
'rate': rate,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _get_or_create_currency(cls, name, symbol):
|
||||
"""Get or create a currency based on name. This makes the test
|
||||
non-reliant on demo data."""
|
||||
currency = cls.env['res.currency'].search([('name', '=', name)])
|
||||
return currency or currency.create({
|
||||
'name': name,
|
||||
'symbol': symbol,
|
||||
})
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
|
||||
def _setup_pricelist(self, currency_ratio=2):
|
||||
to_currency = self._setup_currency(currency_ratio)
|
||||
|
||||
discount = 10
|
||||
|
||||
pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'test pl',
|
||||
'currency_id': to_currency.id,
|
||||
'company_id': self.computer.company_id.id,
|
||||
})
|
||||
|
||||
pricelist_item = self.env['product.pricelist.item'].create({
|
||||
'min_quantity': 2,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': discount,
|
||||
'pricelist_id': pricelist.id,
|
||||
})
|
||||
|
||||
return (pricelist, pricelist_item, currency_ratio, 1 - discount / 100)
|
||||
|
||||
def test_01_is_combination_possible_archived(self):
|
||||
"""The goal is to test the possibility of archived combinations.
|
||||
|
||||
This test could not be put into product module because there was no
|
||||
model which had product_id as required and without cascade on delete.
|
||||
Here we have the sales order line in this situation.
|
||||
|
||||
This is a necessary condition for `_create_variant_ids` to archive
|
||||
instead of delete the variants.
|
||||
"""
|
||||
def do_test(self):
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2)
|
||||
|
||||
variant = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1)
|
||||
variant2 = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_2)
|
||||
|
||||
self.assertTrue(variant)
|
||||
self.assertTrue(variant2)
|
||||
|
||||
# Create a dummy SO to prevent the variant from being deleted by
|
||||
# _create_variant_ids() because the variant is a related field that
|
||||
# is required on the SO line
|
||||
so = self.env['sale.order'].create({'partner_id': 1})
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'name': "test",
|
||||
'product_id': variant.id
|
||||
})
|
||||
# additional variant to test correct ignoring when mismatch values
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'name': "test",
|
||||
'product_id': variant2.id
|
||||
})
|
||||
|
||||
variant2.active = False
|
||||
# CASE: 1 not archived, 2 archived
|
||||
self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
|
||||
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_2))
|
||||
# CASE: both archived combination (without no_variant)
|
||||
variant.active = False
|
||||
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_2))
|
||||
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
|
||||
|
||||
# CASE: OK after attribute line removed
|
||||
self.computer_hdd_attribute_lines.write({'active': False})
|
||||
self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8))
|
||||
|
||||
# CASE: not archived (with no_variant)
|
||||
self.hdd_attribute.create_variant = 'no_variant'
|
||||
self._add_hdd_attribute_line()
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2)
|
||||
|
||||
self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
|
||||
|
||||
# CASE: archived combination found (with no_variant)
|
||||
variant = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1)
|
||||
variant.active = False
|
||||
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
|
||||
|
||||
# CASE: archived combination has different attributes (including no_variant)
|
||||
self.computer_ssd_attribute_lines.write({'active': False})
|
||||
|
||||
variant4 = self.computer._get_variant_for_combination(computer_ram_8 + computer_hdd_1)
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'name': "test",
|
||||
'product_id': variant4.id
|
||||
})
|
||||
self.assertTrue(self.computer._is_combination_possible(computer_ram_8 + computer_hdd_1))
|
||||
|
||||
# CASE: archived combination has different attributes (without no_variant)
|
||||
self.computer_hdd_attribute_lines.write({'active': False})
|
||||
self.hdd_attribute.create_variant = 'always'
|
||||
self._add_hdd_attribute_line()
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2)
|
||||
|
||||
variant5 = self.computer._get_variant_for_combination(computer_ram_8 + computer_hdd_1)
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'name': "test",
|
||||
'product_id': variant5.id
|
||||
})
|
||||
|
||||
self.assertTrue(variant4 != variant5)
|
||||
|
||||
self.assertTrue(self.computer._is_combination_possible(computer_ram_8 + computer_hdd_1))
|
||||
|
||||
computer_ssd_256_before = self._get_product_template_attribute_value(self.ssd_256)
|
||||
|
||||
do_test(self)
|
||||
|
||||
# CASE: add back the removed attribute and try everything again
|
||||
self.computer_ssd_attribute_lines = self.env['product.template.attribute.line'].create({
|
||||
'product_tmpl_id': self.computer.id,
|
||||
'attribute_id': self.ssd_attribute.id,
|
||||
'value_ids': [(6, 0, [self.ssd_256.id, self.ssd_512.id])],
|
||||
})
|
||||
|
||||
computer_ssd_256_after = self._get_product_template_attribute_value(self.ssd_256)
|
||||
self.assertEqual(computer_ssd_256_after, computer_ssd_256_before)
|
||||
self.assertEqual(computer_ssd_256_after.attribute_line_id, computer_ssd_256_before.attribute_line_id)
|
||||
do_test(self)
|
||||
|
||||
def test_02_get_combination_info(self):
|
||||
# If using multi-company, company_id will be False, and this code should
|
||||
# still work.
|
||||
# The case with a company_id will be implicitly tested on website_sale.
|
||||
self.computer.company_id = False
|
||||
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
|
||||
# CASE: no pricelist, no currency, with existing combination, with price_extra on attributes
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
computer_variant = self.computer._get_variant_for_combination(combination)
|
||||
|
||||
res = self.computer._get_combination_info(combination)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], computer_variant.id)
|
||||
self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)")
|
||||
self.assertEqual(res['price'], 2222)
|
||||
self.assertEqual(res['list_price'], 2222)
|
||||
self.assertEqual(res['price_extra'], 222)
|
||||
|
||||
# CASE: no combination, product given
|
||||
res = self.computer._get_combination_info(self.env['product.template.attribute.value'], computer_variant.id)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], computer_variant.id)
|
||||
self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)")
|
||||
self.assertEqual(res['price'], 2222)
|
||||
self.assertEqual(res['list_price'], 2222)
|
||||
self.assertEqual(res['price_extra'], 222)
|
||||
|
||||
# CASE: using pricelist, quantity rule
|
||||
pricelist, pricelist_item, currency_ratio, discount_ratio = self._setup_pricelist()
|
||||
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], computer_variant.id)
|
||||
self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)")
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
|
||||
# CASE: no_variant combination, it's another variant now
|
||||
|
||||
self.computer_ssd_attribute_lines.write({'active': False})
|
||||
self.ssd_attribute.create_variant = 'no_variant'
|
||||
self._add_ssd_attribute_line()
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
|
||||
computer_variant_new = self.computer._get_variant_for_combination(combination)
|
||||
self.assertTrue(computer_variant_new)
|
||||
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], computer_variant_new.id)
|
||||
self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To)")
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
|
||||
# CASE: dynamic combination, but the variant already exists
|
||||
self.computer_hdd_attribute_lines.write({'active': False})
|
||||
self.hdd_attribute.create_variant = 'dynamic'
|
||||
self._add_hdd_attribute_line()
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
|
||||
computer_variant_new = self.computer._create_product_variant(combination)
|
||||
self.assertTrue(computer_variant_new)
|
||||
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], computer_variant_new.id)
|
||||
self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To)")
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
|
||||
# CASE: dynamic combination, no variant existing
|
||||
# Test invalidate_cache on product.template _create_variant_ids
|
||||
self._add_keyboard_attribute()
|
||||
combination += self._get_product_template_attribute_value(self.keyboard_excluded)
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], False)
|
||||
self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To, Excluded)")
|
||||
self.assertEqual(res['price'], (2222 - 5) * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], (2222 - 5) * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], (222 - 5) * currency_ratio)
|
||||
|
||||
# CASE: pricelist set value to 0, no variant
|
||||
# Test invalidate_cache on product.pricelist write
|
||||
pricelist_item.percent_price = 100
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['product_template_id'], self.computer.id)
|
||||
self.assertEqual(res['product_id'], False)
|
||||
self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To, Excluded)")
|
||||
self.assertEqual(res['price'], 0)
|
||||
self.assertEqual(res['list_price'], (2222 - 5) * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], (222 - 5) * currency_ratio)
|
||||
|
||||
def test_03_get_combination_info_discount_policy(self):
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
|
||||
pricelist, pricelist_item, currency_ratio, discount_ratio = self._setup_pricelist()
|
||||
|
||||
pricelist.discount_policy = 'with_discount'
|
||||
|
||||
# CASE: no discount, setting with_discount
|
||||
res = self.computer._get_combination_info(combination, add_qty=1, pricelist=pricelist)
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
self.assertEqual(res['has_discounted_price'], False)
|
||||
|
||||
# CASE: discount, setting with_discount
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
self.assertEqual(res['has_discounted_price'], False)
|
||||
|
||||
# CASE: no discount, setting without_discount
|
||||
pricelist.discount_policy = 'without_discount'
|
||||
res = self.computer._get_combination_info(combination, add_qty=1, pricelist=pricelist)
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
self.assertEqual(res['has_discounted_price'], False)
|
||||
|
||||
# CASE: discount, setting without_discount
|
||||
res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist)
|
||||
self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio)
|
||||
self.assertEqual(res['list_price'], 2222 * currency_ratio)
|
||||
self.assertEqual(res['price_extra'], 222 * currency_ratio)
|
||||
self.assertEqual(res['has_discounted_price'], True)
|
||||
|
||||
def test_04_create_product_variant_non_dynamic(self):
|
||||
"""The goal of this test is to make sure the create_product_variant does
|
||||
not create variant if the type is not dynamic. It can however return a
|
||||
variant if it already exists."""
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_ram_16 = self._get_product_template_attribute_value(self.ram_16)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
self._add_exclude(computer_ram_16, computer_hdd_1)
|
||||
|
||||
# CASE: variant is already created, it should return it
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
variant1 = self.computer._get_variant_for_combination(combination)
|
||||
self.assertEqual(self.computer._create_product_variant(combination), variant1)
|
||||
|
||||
# CASE: variant does not exist, but template is non-dynamic, so it
|
||||
# should not create it
|
||||
Product = self.env['product.product']
|
||||
variant1.unlink()
|
||||
self.assertEqual(self.computer._create_product_variant(combination), Product)
|
||||
|
||||
def test_05_create_product_variant_dynamic(self):
|
||||
"""The goal of this test is to make sure the create_product_variant does
|
||||
work with dynamic. If the combination is possible, it should create it.
|
||||
If it's not possible, it should not create it."""
|
||||
self.computer_hdd_attribute_lines.write({'active': False})
|
||||
self.hdd_attribute.create_variant = 'dynamic'
|
||||
self._add_hdd_attribute_line()
|
||||
|
||||
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
|
||||
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
|
||||
computer_ram_16 = self._get_product_template_attribute_value(self.ram_16)
|
||||
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
|
||||
self._add_exclude(computer_ram_16, computer_hdd_1)
|
||||
|
||||
# CASE: variant does not exist, but combination is not possible
|
||||
# so it should not create it
|
||||
impossible_combination = computer_ssd_256 + computer_ram_16 + computer_hdd_1
|
||||
Product = self.env['product.product']
|
||||
self.assertEqual(self.computer._create_product_variant(impossible_combination), Product)
|
||||
|
||||
# CASE: the variant does not exist, and the combination is possible, so
|
||||
# it should create it
|
||||
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
|
||||
variant = self.computer._create_product_variant(combination)
|
||||
self.assertTrue(variant)
|
||||
|
||||
# CASE: the variant already exists, so it should return it
|
||||
self.assertEqual(variant, self.computer._create_product_variant(combination))
|
||||
|
||||
def _add_keyboard_attribute(self):
|
||||
self.keyboard_attribute = self.env['product.attribute'].create({
|
||||
'name': 'Keyboard',
|
||||
'sequence': 6,
|
||||
'create_variant': 'dynamic',
|
||||
})
|
||||
self.keyboard_included = self.env['product.attribute.value'].create({
|
||||
'name': 'Included',
|
||||
'attribute_id': self.keyboard_attribute.id,
|
||||
'sequence': 1,
|
||||
})
|
||||
self.keyboard_excluded = self.env['product.attribute.value'].create({
|
||||
'name': 'Excluded',
|
||||
'attribute_id': self.keyboard_attribute.id,
|
||||
'sequence': 2,
|
||||
})
|
||||
self.computer_keyboard_attribute_lines = self.env['product.template.attribute.line'].create({
|
||||
'product_tmpl_id': self.computer.id,
|
||||
'attribute_id': self.keyboard_attribute.id,
|
||||
'value_ids': [(6, 0, [self.keyboard_included.id, self.keyboard_excluded.id])],
|
||||
})
|
||||
self.computer_keyboard_attribute_lines.product_template_value_ids[0].price_extra = 5
|
||||
self.computer_keyboard_attribute_lines.product_template_value_ids[1].price_extra = -5
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleProductVariants(ProductAttributesCommon, SaleCommon):
|
||||
|
||||
# TODO move to sale_product_configurator tests in 16.3+
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.product_template_2lines_2attributes = cls.env['product.template'].create({
|
||||
'name': '2 lines 2 attributes',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id,
|
||||
'categ_id': cls.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': cls.color_attribute.id,
|
||||
'value_ids': [Command.set([
|
||||
cls.color_attribute_red.id,
|
||||
cls.color_attribute_blue.id,
|
||||
])],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': cls.size_attribute.id,
|
||||
'value_ids': [Command.set([
|
||||
cls.size_attribute_s.id,
|
||||
cls.size_attribute_m.id,
|
||||
])]
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Sell all variants
|
||||
cls.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id,
|
||||
})
|
||||
for product in cls.product_template_2lines_2attributes.product_variant_ids
|
||||
]
|
||||
|
||||
def test_attribute_removal(self):
|
||||
def _get_ptavs():
|
||||
return self.product_template_2lines_2attributes.with_context(
|
||||
active_test=False
|
||||
).attribute_line_ids.product_template_value_ids
|
||||
|
||||
def _get_archived_variants():
|
||||
return self.product_template_2lines_2attributes.with_context(
|
||||
active_test=False
|
||||
).product_variant_ids.filtered(lambda p: not p.active)
|
||||
|
||||
def _get_active_variants():
|
||||
return self.product_template_2lines_2attributes.product_variant_ids
|
||||
|
||||
self.assertEqual(len(_get_ptavs()), 4)
|
||||
self.product_template_2lines_2attributes.attribute_line_ids = [
|
||||
Command.unlink(self.product_template_2lines_2attributes.attribute_line_ids.filtered(
|
||||
lambda ptal: ptal.attribute_id.id == self.size_attribute.id
|
||||
).id)
|
||||
]
|
||||
self.assertEqual(len(_get_ptavs()), 4)
|
||||
|
||||
# Use products s.t. they are archived and not deleted
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id,
|
||||
})
|
||||
for product in self.product_template_2lines_2attributes.product_variant_ids
|
||||
]
|
||||
|
||||
self.assertEqual(len(_get_archived_variants()), 4)
|
||||
self.assertEqual(len(_get_active_variants()), 2)
|
||||
|
||||
self.product_template_2lines_2attributes.attribute_line_ids = [
|
||||
Command.create({
|
||||
'attribute_id': self.size_attribute.id,
|
||||
'value_ids': [Command.set([
|
||||
self.size_attribute_s.id,
|
||||
])]
|
||||
})
|
||||
]
|
||||
self.assertEqual(len(_get_ptavs()), 4)
|
||||
self.assertEqual(len(_get_active_variants()), 2)
|
||||
self.assertEqual(len(_get_archived_variants()), 4)
|
||||
|
||||
# When adding a single attribute line, the attribute will be added to all existing variants
|
||||
# Instead of unarchiving existing archived variants with the same combination
|
||||
# Leading to a state where the database holds two variants with the same combination
|
||||
# We don't want this combination to be excluded from the product configurator as it is valid
|
||||
# as long as there is one active variant with this configuration.
|
||||
exclusions_data = self.product_template_2lines_2attributes._get_attribute_exclusions()
|
||||
self.assertTrue(
|
||||
all(
|
||||
tuple(product.product_template_attribute_value_ids.ids) not in exclusions_data['archived_combinations']
|
||||
for product in _get_active_variants()
|
||||
)
|
||||
)
|
||||
405
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_refund.py
Normal file
405
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_refund.py
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleRefund(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
|
||||
|
||||
# Confirm the SO
|
||||
cls.sale_order.action_confirm()
|
||||
|
||||
# Create an invoice with invoiceable lines only
|
||||
payment = cls.env['sale.advance.payment.inv'].with_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,
|
||||
}).create({
|
||||
'advance_payment_method': 'delivered'
|
||||
})
|
||||
payment.create_invoices()
|
||||
|
||||
cls.invoice = cls.sale_order.invoice_ids[0]
|
||||
|
||||
def test_refund_create(self):
|
||||
# Validate invoice
|
||||
self.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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered qty are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEqual(line.qty_to_invoice, 0.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||||
self.assertEqual(line.qty_invoiced, 5.0, "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, 0.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||||
self.assertEqual(line.qty_invoiced, 3.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")
|
||||
self.assertEqual(len(line.invoice_lines), 1, "The lines 'ordered' qty are invoiced, so it should be linked to 1 invoice lines")
|
||||
|
||||
# Make a credit note
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({'active_ids': [self.invoice.id], 'active_id': self.invoice.id, 'active_model': 'account.move'}).create({
|
||||
'refund_method': 'refund', # this is the only mode for which the SO line is linked to the refund (https://github.com/odoo/odoo/commit/e680f29560ac20133c7af0c6364c6ef494662eac)
|
||||
'reason': 'reason test create',
|
||||
'journal_id': self.invoice.journal_id.id,
|
||||
})
|
||||
credit_note_wizard.reverse_moves()
|
||||
invoice_refund = self.sale_order.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1] # the first invoice, its refund, and the new invoice
|
||||
|
||||
# Check invoice's type and number
|
||||
self.assertEqual(invoice_refund.move_type, 'out_refund', 'The last created invoiced should be a refund')
|
||||
self.assertEqual(invoice_refund.state, 'draft', 'Last Customer invoices should be in draft')
|
||||
self.assertEqual(self.sale_order.invoice_count, 2, "The SO should have 2 related invoices: the original, the new refund")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_refund')), 1, "The SO should be linked to only one refund")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_invoice')), 1, "The SO should be linked to only one customer invoices")
|
||||
|
||||
# At this time, the invoice 1 is opend (validated) and its refund is in draft, so the amounts invoiced are not zero for
|
||||
# invoiced sale line. The amounts only take validated invoice/refund 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 same as ordered quantity")
|
||||
self.assertEqual(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO line based on delivered qty")
|
||||
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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEqual(line.qty_to_invoice, 5.0, "As the refund is created, the invoiced quantity cancel each other (consu ordered)")
|
||||
self.assertEqual(line.qty_invoiced, 0.0, "The qty to invoice should have decreased as the refund is created for ordered consu SO line")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 0.0, "Amount to invoice is zero as the refund is not validated")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, line.price_unit * 5, "Amount invoiced is now set as unit price * ordered qty - refund qty) even if the ")
|
||||
self.assertEqual(len(line.invoice_lines), 2, "The line 'ordered consumable' is invoiced, so it should be linked to 2 invoice lines (invoice and refund)")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, 3.0, "As the refund is created, the invoiced quantity cancel each other (consu ordered)")
|
||||
self.assertEqual(line.qty_invoiced, 0.0, "The qty to invoice should have decreased as the refund is created for ordered service SO line")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 0.0, "Amount to invoice is zero as the refund is not validated")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, line.price_unit * 3, "Amount invoiced is now set as unit price * ordered qty - refund qty) even if the ")
|
||||
self.assertEqual(len(line.invoice_lines), 2, "The line 'ordered service' is invoiced, so it should be linked to 2 invoice lines (invoice and refund)")
|
||||
|
||||
# Validate the refund
|
||||
invoice_refund.action_post()
|
||||
|
||||
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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEqual(line.qty_to_invoice, 5.0, "As the refund still exists, the quantity to invoice is the ordered quantity")
|
||||
self.assertEqual(line.qty_invoiced, 0.0, "The qty to invoice should be zero as, with the refund, the quantities cancel each other")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_unit * 5, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, as refund is validated")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "Amount invoiced decreased as the refund is now confirmed")
|
||||
self.assertEqual(len(line.invoice_lines), 2, "The line 'ordered consumable' is invoiced, so it should be linked to 2 invoice lines (invoice and refund)")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, 3.0, "As the refund still exists, the quantity to invoice is the ordered quantity")
|
||||
self.assertEqual(line.qty_invoiced, 0.0, "The qty to invoice should be zero as, with the refund, the quantities cancel each other")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_unit * 3, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, as refund is validated")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "Amount invoiced decreased as the refund is now confirmed")
|
||||
self.assertEqual(len(line.invoice_lines), 2, "The line 'ordered service' is invoiced, so it should be linked to 2 invoice lines (invoice and refund)")
|
||||
|
||||
def test_refund_cancel(self):
|
||||
""" Test invoice with a refund in 'cancel' mode, meaning a refund will be created and auto confirm to completely cancel the first
|
||||
customer invoice. The SO will have 2 invoice (customer + refund) in a paid state at the end. """
|
||||
# Increase quantity of an invoice lines
|
||||
with Form(self.invoice) as invoice_form:
|
||||
with invoice_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.quantity = 6
|
||||
with invoice_form.invoice_line_ids.edit(1) as line_form:
|
||||
line_form.quantity = 4
|
||||
|
||||
# Validate invoice
|
||||
self.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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered qty are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
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")
|
||||
self.assertEqual(len(line.invoice_lines), 1, "The lines 'ordered' qty are invoiced, so it should be linked to 1 invoice lines")
|
||||
|
||||
self.assertEqual(line.qty_invoiced, line.product_uom_qty + 1, "The quantity invoiced is +1 unit from the one of the sale line, as we modified invoice lines (%s)" % (line.name,))
|
||||
self.assertEqual(line.qty_to_invoice, -1, "The quantity to invoice is negative as we invoice more than ordered")
|
||||
|
||||
# Make a credit note
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({'active_ids': self.invoice.ids, 'active_id': self.invoice.id, 'active_model': 'account.move'}).create({
|
||||
'refund_method': 'cancel',
|
||||
'reason': 'reason test cancel',
|
||||
'journal_id': self.invoice.journal_id.id,
|
||||
})
|
||||
invoice_refund = self.env['account.move'].browse(credit_note_wizard.reverse_moves()['res_id'])
|
||||
|
||||
# Check invoice's type and number
|
||||
self.assertEqual(invoice_refund.move_type, 'out_refund', 'The last created invoiced should be a customer invoice')
|
||||
self.assertEqual(invoice_refund.payment_state, 'paid', 'Last Customer creadit note should be in paid state')
|
||||
self.assertEqual(self.sale_order.invoice_count, 2, "The SO should have 3 related invoices: the original, the refund, and the new one")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_refund')), 1, "The SO should be linked to only one refund")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_invoice')), 1, "The SO should be linked to only one customer invoices")
|
||||
|
||||
# At this time, the invoice 1 is opened (validated) and its refund validated too, so the amounts invoiced are zero for
|
||||
# all sale line. All invoiceable Sale lines have
|
||||
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 line based on delivered qty")
|
||||
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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, line.product_uom_qty, "The quantity to invoice should be the ordered quantity")
|
||||
self.assertEqual(line.qty_invoiced, 0, "The quantity invoiced is zero as the refund (paid) completely cancel the first invoice")
|
||||
|
||||
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")
|
||||
self.assertEqual(len(line.invoice_lines), 2, "The lines 'ordered' qty are invoiced, so it should be linked to 1 invoice lines")
|
||||
|
||||
def test_refund_modify(self):
|
||||
""" Test invoice with a refund in 'modify' mode, and check customer invoices credit note is created from respective invoice """
|
||||
# Decrease quantity of an invoice lines
|
||||
with Form(self.invoice) as invoice_form:
|
||||
with invoice_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.quantity = 3
|
||||
with invoice_form.invoice_line_ids.edit(1) as line_form:
|
||||
line_form.quantity = 2
|
||||
|
||||
# Validate invoice
|
||||
self.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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered qty are not invoiced, so they should not be linked to invoice line")
|
||||
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")
|
||||
self.assertEqual(len(line.invoice_lines), 1, "The lines 'ordered' qty are invoiced, so it should be linked to 1 invoice lines")
|
||||
|
||||
# Make a credit note
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({'active_ids': [self.invoice.id], 'active_id': self.invoice.id, 'active_model': 'account.move'}).create({
|
||||
'refund_method': 'modify', # this is the only mode for which the SO line is linked to the refund (https://github.com/odoo/odoo/commit/e680f29560ac20133c7af0c6364c6ef494662eac)
|
||||
'reason': 'reason test modify',
|
||||
'journal_id': self.invoice.journal_id.id,
|
||||
})
|
||||
invoice_refund = self.env['account.move'].browse(credit_note_wizard.reverse_moves()['res_id'])
|
||||
|
||||
# Check invoice's type and number
|
||||
self.assertEqual(invoice_refund.move_type, 'out_invoice', 'The last created invoiced should be a customer invoice')
|
||||
self.assertEqual(invoice_refund.state, 'draft', 'Last Customer invoices should be in draft')
|
||||
self.assertEqual(self.sale_order.invoice_count, 3, "The SO should have 3 related invoices: the original, the refund, and the new one")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_refund')), 1, "The SO should be linked to only one refund")
|
||||
self.assertEqual(len(self.sale_order.invoice_ids.filtered(lambda inv: inv.move_type == 'out_invoice')), 2, "The SO should be linked to two customer invoices")
|
||||
|
||||
# At this time, the invoice 1 and its refund are confirmed, so the amounts invoiced are zero. The third invoice
|
||||
# (2nd customer inv) is in draft state.
|
||||
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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered are not invoiced, so they should not be linked to invoice line")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEqual(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEqual(line.qty_invoiced, 3.0, "The ordered (prod) sale line does not change on invoice 2 confirmation")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_unit * 5, "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, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")
|
||||
self.assertEqual(len(line.invoice_lines), 3, "The line 'ordered consumable' is invoiced, so it should be linked to 3 invoice lines (invoice and refund)")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEqual(line.qty_invoiced, 2.0, "The ordered (serv) sale line does not change on invoice 2 confirmation")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, line.price_unit * 3, "Amount to invoice is now set as unit price * ordered qty - refund qty) even if the ")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")
|
||||
self.assertEqual(len(line.invoice_lines), 3, "The line 'ordered service' is invoiced, so it should be linked to 3 invoice lines (invoice and refund)")
|
||||
|
||||
# Change unit of ordered product on refund lines
|
||||
move_form = Form(invoice_refund)
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 100
|
||||
with move_form.invoice_line_ids.edit(1) as line_form:
|
||||
line_form.price_unit = 50
|
||||
invoice_refund = move_form.save()
|
||||
|
||||
# Validate the refund
|
||||
invoice_refund.action_post()
|
||||
|
||||
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")
|
||||
self.assertFalse(line.invoice_lines, "The line based on delivered are not invoiced, so they should not be linked to invoice line, even after validation")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEqual(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (3)")
|
||||
self.assertEqual(line.qty_invoiced, 3.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 1100.0, "")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 300.0, "")
|
||||
self.assertEqual(len(line.invoice_lines), 3, "The line 'ordered consumable' is invoiced, so it should be linked to 2 invoice lines (invoice and refund), even after validation")
|
||||
else:
|
||||
self.assertEqual(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (3)")
|
||||
self.assertEqual(line.qty_invoiced, 2.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 170.0, "")
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 100.0, "")
|
||||
self.assertEqual(len(line.invoice_lines), 3, "The line 'ordered service' is invoiced, so it should be linked to 2 invoice lines (invoice and refund), even after validation")
|
||||
|
||||
def test_refund_invoice_with_downpayment(self):
|
||||
sale_order_refund = 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,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
sol_product = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 5,
|
||||
'order_id': sale_order_refund.id,
|
||||
'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],
|
||||
'active_id': sale_order_refund.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
}
|
||||
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(so_context).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 50,
|
||||
'deposit_account_id': self.company_data['default_account_revenue'].id
|
||||
})
|
||||
downpayment.create_invoices()
|
||||
# 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
|
||||
})
|
||||
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.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()
|
||||
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({'active_ids': [so_invoice.id], 'active_id': so_invoice.id, 'active_model': 'account.move'}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'reason test refund with downpayment',
|
||||
'journal_id': so_invoice.journal_id.id,
|
||||
})
|
||||
credit_note_wizard.reverse_moves()
|
||||
invoice_refund = sale_order_refund.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1]
|
||||
invoice_refund.action_post()
|
||||
|
||||
self.assertEqual(sol_product.qty_to_invoice, 5.0, "As the refund still exists, the quantity to invoice is the ordered quantity")
|
||||
self.assertEqual(sol_product.qty_invoiced, 0.0, "The qty invoiced should be zero as, with the refund, the quantities cancel each other")
|
||||
self.assertEqual(sol_product.untaxed_amount_to_invoice, sol_product.price_unit * 5, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, as refund is validated")
|
||||
self.assertEqual(sol_product.untaxed_amount_invoiced, 0.0, "Amount invoiced decreased as the refund is now confirmed")
|
||||
self.assertEqual(len(sol_product.invoice_lines), 2, "The product line is invoiced, so it should be linked to 2 invoice lines (invoice and refund)")
|
||||
|
||||
self.assertEqual(sol_downpayment.qty_to_invoice, -1.0, "As the downpayment was invoiced separately, it will still have to be deducted from the total invoice (hence -1.0), after the refund.")
|
||||
self.assertEqual(sol_downpayment.qty_invoiced, 1.0, "The qty to invoice should be 1 as, with the refund, the products are not invoiced anymore, but the downpayment still is")
|
||||
self.assertEqual(sol_downpayment.untaxed_amount_to_invoice, -(sol_product.price_unit * 5)/2, "Amount to invoice decreased as the refund is now confirmed")
|
||||
self.assertEqual(sol_downpayment.untaxed_amount_invoiced, (sol_product.price_unit * 5)/2, "Amount invoiced is now set as half of all products' total amount to invoice, as refund is validated")
|
||||
self.assertEqual(len(sol_downpayment.invoice_lines), 3, "The product line is invoiced, so it should be linked to 3 invoice lines (downpayment invoice, partial invoice and refund)")
|
||||
127
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_report.py
Normal file
127
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_report.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# -*- 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 tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleReportCurrencyRate(SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.usd_cmp = cls.env['res.company'].create({
|
||||
'name': 'USD Company', 'currency_id': cls.env.ref('base.USD').id,
|
||||
})
|
||||
cls.eur_cmp = cls.env['res.company'].create({
|
||||
'name': 'EUR Company', 'currency_id': cls.env.ref('base.EUR').id,
|
||||
})
|
||||
|
||||
def test_sale_report_foreign_currency(self):
|
||||
"""
|
||||
Test that amounts are correctly converted between currencies.
|
||||
There are two different conversions to take into account:
|
||||
- currency of the sale order pricelist -> currency of the sale order company
|
||||
- currency of sale order company -> currency of the current user company
|
||||
Adjustment between past and present rates must also be taken into account.
|
||||
"""
|
||||
|
||||
companies = self.usd_cmp + self.eur_cmp
|
||||
today = fields.Date.today()
|
||||
past_day = fields.Date.to_date('2020-01-01')
|
||||
usd = self.usd_cmp.currency_id
|
||||
eur = self.eur_cmp.currency_id
|
||||
ars = self._enable_currency('ARS')
|
||||
|
||||
# Create corresponding pricelists and rates.
|
||||
pricelists = self.env['product.pricelist'].create([
|
||||
{'name': 'Pricelist (USD)', 'currency_id': usd.id},
|
||||
{'name': 'Pricelist (EUR)', 'currency_id': eur.id},
|
||||
{'name': 'Pricelist (ARS)', 'currency_id': ars.id},
|
||||
])
|
||||
self.env['res.currency.rate'].create([
|
||||
{'name': past_day, 'rate': 555, 'currency_id': ars.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': past_day, 'rate': 1.0, 'currency_id': eur.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': past_day, 'rate': 999, 'currency_id': usd.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': past_day, 'rate': 3.0, 'currency_id': ars.id, 'company_id': self.usd_cmp.id},
|
||||
{'name': past_day, 'rate': 0.1, 'currency_id': eur.id, 'company_id': self.usd_cmp.id},
|
||||
{'name': past_day, 'rate': 1.0, 'currency_id': usd.id, 'company_id': self.usd_cmp.id},
|
||||
{'name': today, 'rate': 222, 'currency_id': ars.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': today, 'rate': 1.0, 'currency_id': eur.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': today, 'rate': 2.9, 'currency_id': usd.id, 'company_id': self.eur_cmp.id},
|
||||
{'name': today, 'rate': 101, 'currency_id': ars.id, 'company_id': self.usd_cmp.id},
|
||||
{'name': today, 'rate': 0.6, 'currency_id': eur.id, 'company_id': self.usd_cmp.id},
|
||||
{'name': today, 'rate': 1.0, 'currency_id': usd.id, 'company_id': self.usd_cmp.id},
|
||||
])
|
||||
|
||||
self.assertEqual(self.product.currency_id, usd)
|
||||
|
||||
# Needed to get conversion rates between companies.
|
||||
currency_rates = (companies + self.env.company).mapped('currency_id')._get_rates(
|
||||
self.env.company, today
|
||||
)
|
||||
|
||||
sale_orders = self.env['sale.order']
|
||||
expected_reported_amount = 0 # The total amount of all sale orders in the report.
|
||||
qty = 0 # to add variety to the data
|
||||
|
||||
# Create sale orders
|
||||
for company in companies:
|
||||
SaleOrder = self.env['sale.order'].with_company(company)
|
||||
for date in (past_day, today):
|
||||
for pricelist in pricelists:
|
||||
qty += 1
|
||||
order = SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'date_order': date,
|
||||
'order_line': [Command.create(
|
||||
{'product_id': self.product.id, 'product_uom_qty': qty}
|
||||
)],
|
||||
})
|
||||
sale_orders |= order
|
||||
|
||||
expected_so_currency_rate = self.env['res.currency.rate'].search([
|
||||
('name', '=', date),
|
||||
('currency_id', '=', pricelist.currency_id.id),
|
||||
('company_id', '=', company.id),
|
||||
]).rate
|
||||
expected_product_currency_rate = self.env['res.currency.rate'].search([
|
||||
('name', '=', date),
|
||||
('currency_id', '=', self.product.currency_id.id),
|
||||
('company_id', '=', company.id),
|
||||
]).rate
|
||||
|
||||
# To find the total amount we convert the price of the product from its currency
|
||||
# to the currency of the so company and then from it to the currency of the so
|
||||
# pricelist.
|
||||
price_for_so_company = self.product.list_price / expected_product_currency_rate
|
||||
expected_rounded_price = pricelist.currency_id.round(
|
||||
price_for_so_company * expected_so_currency_rate
|
||||
)
|
||||
|
||||
expected_amount_total = qty * expected_rounded_price
|
||||
self.assertAlmostEqual(order.currency_rate, expected_so_currency_rate)
|
||||
self.assertAlmostEqual(order.amount_total, expected_amount_total)
|
||||
|
||||
# The amount in the report is converted first to the currency of the company and
|
||||
# then to the currency of the current company (self.env.company).
|
||||
current_company_rate = currency_rates[self.env.company.currency_id.id]
|
||||
so_company_rate = currency_rates[company.currency_id.id]
|
||||
conversion_rate = (current_company_rate / so_company_rate)
|
||||
expected_reported_amount += (
|
||||
order.amount_total / order.currency_rate * conversion_rate
|
||||
)
|
||||
|
||||
# The report should show the amount in the current (in this case usd) company currency.
|
||||
report_lines = self.env['sale.report'].sudo().with_context(
|
||||
allow_company_ids=[self.usd_cmp.id, self.eur_cmp.id]
|
||||
).search([('order_id', 'in', sale_orders.ids)])
|
||||
|
||||
price_total = sum(report_lines.mapped('price_total'))
|
||||
self.assertEqual(price_total, expected_reported_amount)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.test_invoice_tax_totals import TestTaxTotals
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class SaleTestTaxTotals(TestTaxTotals):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.so_product = cls.env['product.product'].create({
|
||||
'name': 'Odoo course',
|
||||
'type': 'service',
|
||||
})
|
||||
|
||||
def _create_document_for_tax_totals_test(self, lines_data):
|
||||
# Overridden in order to run the inherited tests with sale.order's
|
||||
# tax_totals field instead of account.move's
|
||||
|
||||
lines_vals = [
|
||||
(0, 0, {
|
||||
'name': 'test',
|
||||
'product_id': self.so_product.id,
|
||||
'price_unit': amount,
|
||||
'product_uom_qty': 1,
|
||||
'tax_id': [(6, 0, taxes.ids)],
|
||||
})
|
||||
for amount, taxes in lines_data]
|
||||
|
||||
return self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': lines_vals,
|
||||
})
|
||||
955
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_to_invoice.py
Normal file
955
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_to_invoice.py
Normal file
|
|
@ -0,0 +1,955 @@
|
|||
# -*- 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_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({'price_include': True})
|
||||
# Let's do an invoice for a deposit of 100
|
||||
product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
|
||||
product_id = self.env['product.product'].browse(int(product_id)).exists()
|
||||
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',
|
||||
'refund_method': 'refund',
|
||||
'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, self.sol_serv_deliver.qty_delivered * self.sol_serv_deliver.price_reduce, "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_template(email_ctx.get('default_template_id'))
|
||||
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,
|
||||
'price_unit': 30,
|
||||
}),
|
||||
],
|
||||
})
|
||||
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)
|
||||
self.assertEqual(line.untaxed_amount_invoiced, 300)
|
||||
|
||||
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')
|
||||
Loading…
Add table
Add a link
Reference in a new issue