mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 16:52:03 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_access_rights
|
||||
|
|
@ -6,16 +5,25 @@ from . import test_accrued_sale_orders
|
|||
from . import test_common
|
||||
from . import test_controllers
|
||||
from . import test_credit_limit
|
||||
from . import test_ir_config_parameter
|
||||
from . import test_onchange
|
||||
from . import test_payment_flow
|
||||
from . import test_reinvoice
|
||||
from . import test_product_attribute_value
|
||||
from . import test_product_catalog
|
||||
from . import test_product_configurator_data
|
||||
from . import test_sale_combo_configurator
|
||||
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_order_product_catalog
|
||||
from . import test_sale_order_ui
|
||||
from . import test_sale_prices
|
||||
from . import test_sale_product_attribute_value_config
|
||||
from . import test_sale_product_template
|
||||
from . import test_sale_refund
|
||||
from . import test_sale_tax_totals
|
||||
from . import test_sale_to_invoice
|
||||
from . import test_sale_report
|
||||
from . import test_sale_sections
|
||||
from . import test_taxes_downpayment
|
||||
from . import test_taxes_global_discount
|
||||
from . import test_taxes_tax_totals_summary
|
||||
from . import test_import_files
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
# -*- 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.account.tests.common import AccountTestInvoicingCommon, TestTaxCommon
|
||||
from odoo.addons.product.tests.common import ProductCommon
|
||||
from odoo.addons.sales_team.tests.common import SalesTeamCommon
|
||||
|
||||
|
|
@ -18,223 +16,239 @@ class SaleCommon(
|
|||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.company.country_id = cls.env.ref('base.us')
|
||||
cls.env.company.country_id = cls.quick_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.group_discount_per_so_line = cls.quick_ref('sale.group_discount_per_so_line')
|
||||
|
||||
cls.empty_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
(cls.product + cls.service_product).write({
|
||||
'taxes_id': [Command.clear()],
|
||||
})
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
cls._enable_pricelists()
|
||||
cls.empty_order, cls.sale_order = cls.env['sale.order'].create([
|
||||
{
|
||||
'partner_id': cls.partner.id,
|
||||
}, {
|
||||
'partner_id': cls.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': cls.product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.service_product.id,
|
||||
'product_uom_qty': 12.5,
|
||||
})
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def _enable_discounts(cls):
|
||||
cls.env.user.group_ids += cls.group_discount_per_so_line
|
||||
|
||||
def _create_so(self, **values):
|
||||
default_values = {
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': cls.consumable_product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
'product_id': self.product.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.service_product.id,
|
||||
'product_uom_qty': 12.5,
|
||||
})
|
||||
]
|
||||
})
|
||||
],
|
||||
**values
|
||||
}
|
||||
return self.env['sale.order'].create(default_values)
|
||||
|
||||
|
||||
class TestSaleCommon(AccountTestInvoicingCommon):
|
||||
|
||||
@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,
|
||||
}),
|
||||
def collect_company_accounting_data(cls, company):
|
||||
company_data = super().collect_company_accounting_data(company)
|
||||
|
||||
company_data.update({
|
||||
# Users
|
||||
'default_user_salesman': Users.create({
|
||||
'default_user_salesman': cls.env['res.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])],
|
||||
'group_ids': [(6, 0, cls.quick_ref('sales_team.group_sale_salesman').ids)],
|
||||
'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,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'SERV_DEL',
|
||||
'invoice_policy': 'delivery',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_service_order': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_service_order',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.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, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_order_cost': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_cost',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'cost',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_delivery_cost': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_cost',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'cost',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_order_sales_price': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_sales_price',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'sales_price',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_delivery_sales_price': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_sales_price',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'sales_price',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_order_no': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_order_no',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_9999',
|
||||
'invoice_policy': 'order',
|
||||
'expense_policy': 'no',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
'product_delivery_no': cls.env['product.product'].with_company(company).create({
|
||||
'name': 'product_delivery_no',
|
||||
'categ_id': company_data['product_category'].id,
|
||||
'categ_id': cls.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,
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'default_code': 'FURN_7777',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'no',
|
||||
'taxes_id': [(6, 0, [])],
|
||||
'supplier_taxes_id': [(6, 0, [])],
|
||||
'company_id': company.id,
|
||||
}),
|
||||
})
|
||||
|
||||
return company_data
|
||||
|
||||
@classmethod
|
||||
def get_default_groups(cls):
|
||||
groups = super().get_default_groups()
|
||||
return groups | cls.quick_ref('sales_team.group_sale_manager')
|
||||
|
||||
class TestSaleCommon(AccountTestInvoicingCommon, TestSaleCommonBase):
|
||||
''' Setup to be used post-install with sale and accounting test configuration.'''
|
||||
|
||||
class TestTaxCommonSale(TestSaleCommon, TestTaxCommon):
|
||||
|
||||
@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,
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.foreign_currency_pricelist = cls.env['product.pricelist'].create({
|
||||
'name': "TestTaxCommonSale",
|
||||
'currency_id': cls.foreign_currency.id,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
|
||||
return company_data
|
||||
def convert_document_to_sale_order(self, document):
|
||||
order_date = '2020-01-01'
|
||||
currency = document['currency']
|
||||
self._ensure_rate(currency, order_date, document['rate'])
|
||||
self.foreign_currency_pricelist.currency_id = currency
|
||||
return self.env['sale.order'].create({
|
||||
'date_order': order_date,
|
||||
'currency_id': currency.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'pricelist_id': self.foreign_currency_pricelist.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'name': str(i),
|
||||
'product_id': (base_line['product_id'] or self.product_a).id,
|
||||
'price_unit': base_line['price_unit'],
|
||||
'discount': base_line['discount'],
|
||||
'product_uom_qty': base_line['quantity'],
|
||||
'tax_ids': [Command.set(base_line['tax_ids'].ids)],
|
||||
})
|
||||
for i, base_line in enumerate(document['lines'])
|
||||
],
|
||||
})
|
||||
|
||||
def assert_sale_order_tax_totals_summary(self, sale_order, expected_values, soft_checking=False):
|
||||
self._assert_tax_totals_summary(sale_order.tax_totals, expected_values, soft_checking=soft_checking)
|
||||
cash_rounding_base_amount_currency = sale_order.tax_totals.get('cash_rounding_base_amount_currency', 0.0)
|
||||
expected_amounts = {}
|
||||
if 'base_amount_currency' in expected_values:
|
||||
expected_amounts['amount_untaxed'] = expected_values['base_amount_currency'] + cash_rounding_base_amount_currency
|
||||
if 'tax_amount_currency' in expected_values:
|
||||
expected_amounts['amount_tax'] = expected_values['tax_amount_currency']
|
||||
if 'total_amount_currency' in expected_values:
|
||||
expected_amounts['amount_total'] = expected_values['total_amount_currency']
|
||||
self.assertRecordValues(sale_order, [expected_amounts])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import HttpCase
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
from odoo.addons.uom.tests.common import UomCommon
|
||||
|
||||
|
||||
class TestProductConfiguratorCommon(UomCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Setup attributes and attributes values
|
||||
cls.product_attribute_1 = cls.env['product.attribute'].create({
|
||||
'name': 'Legs',
|
||||
'sequence': 10,
|
||||
})
|
||||
product_attribute_value_1 = cls.env['product.attribute.value'].create({
|
||||
'name': 'Steel',
|
||||
'attribute_id': cls.product_attribute_1.id,
|
||||
'sequence': 1,
|
||||
})
|
||||
product_attribute_value_2 = cls.env['product.attribute.value'].create({
|
||||
'name': 'Aluminium',
|
||||
'attribute_id': cls.product_attribute_1.id,
|
||||
'sequence': 2,
|
||||
})
|
||||
product_attribute_2 = cls.env['product.attribute'].create({
|
||||
'name': 'Color',
|
||||
'display_type': 'color',
|
||||
'sequence': 20,
|
||||
})
|
||||
product_attribute_value_3 = cls.env['product.attribute.value'].create({
|
||||
'name': 'White',
|
||||
'attribute_id': product_attribute_2.id,
|
||||
'html_color': '#FFFFFF',
|
||||
'sequence': 1,
|
||||
})
|
||||
product_attribute_value_4 = cls.env['product.attribute.value'].create({
|
||||
'name': 'Black',
|
||||
'attribute_id': product_attribute_2.id,
|
||||
'html_color': '#000000',
|
||||
'sequence': 2,
|
||||
})
|
||||
|
||||
# Create product template
|
||||
cls.product_product_custo_desk = cls.env['product.template'].create({
|
||||
'name': 'Customizable Desk (TEST)',
|
||||
'standard_price': 500.0,
|
||||
'list_price': 750.0,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': cls.product_attribute_1.id,
|
||||
'value_ids': [Command.link(product_attribute_value_1.id), Command.link(product_attribute_value_2.id)],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': product_attribute_2.id,
|
||||
'value_ids': [Command.link(product_attribute_value_3.id), Command.link(product_attribute_value_4.id)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Apply a price_extra for the attribute Aluminium
|
||||
cls.product_product_custo_desk.attribute_line_ids[0].product_template_value_ids[1].price_extra = 50.40
|
||||
|
||||
# Add a Custom attribute
|
||||
product_attribute_value_custom = cls.env['product.attribute.value'].create({
|
||||
'name': 'Custom',
|
||||
'attribute_id': cls.product_attribute_1.id,
|
||||
'sequence': 3,
|
||||
'is_custom': True
|
||||
})
|
||||
cls.product_product_custo_desk.attribute_line_ids[0].write({'value_ids': [(4, product_attribute_value_custom.id)]})
|
||||
|
||||
# Disable the aluminium + black product
|
||||
cls.product_product_custo_desk.product_variant_ids[3].active = False
|
||||
|
||||
# Setup a first optional product
|
||||
img_path = 'product/static/img/product_product_11-image.jpg'
|
||||
img_content = base64.b64encode(file_open(img_path, "rb").read())
|
||||
cls.product_product_conf_chair = cls.env['product.template'].create({
|
||||
'name': 'Conference Chair (TEST)',
|
||||
'image_1920': img_content,
|
||||
'list_price': 16.50,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': cls.product_attribute_1.id,
|
||||
'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
cls.product_product_conf_chair.attribute_line_ids[0].product_template_value_ids[1].price_extra = 6.40
|
||||
cls.product_product_custo_desk.optional_product_ids = [(4, cls.product_product_conf_chair.id)]
|
||||
|
||||
# Setup a second optional product
|
||||
cls.product_product_conf_chair_floor_protect = cls.env['product.template'].create({
|
||||
'name': 'Chair floor protection (TEST)',
|
||||
'list_price': 12.0,
|
||||
})
|
||||
cls.product_product_conf_chair.optional_product_ids = [(4, cls.product_product_conf_chair_floor_protect.id)]
|
||||
|
|
@ -1,28 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
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.mail.tests.common import MailCommon
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccessRights(BaseUsersCommon, SaleCommon):
|
||||
class TestAccessRights(SaleCommon, MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_portal = cls._create_new_portal_user()
|
||||
cls.user_internal = cls._create_new_internal_user()
|
||||
|
||||
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)],
|
||||
'group_ids': [(6, 0, cls.group_sale_salesman.ids)],
|
||||
})
|
||||
|
||||
# Create the SO with a specific salesperson
|
||||
|
|
@ -91,6 +94,22 @@ class TestAccessRights(BaseUsersCommon, SaleCommon):
|
|||
# Salesperson can confirm the SO
|
||||
so_as_salesperson.action_confirm()
|
||||
|
||||
# Salesperson can't confirm the related move
|
||||
move_as_salesperson = so_as_salesperson._create_invoices().with_user(self.sale_user2)
|
||||
with self.assertRaises(AccessError):
|
||||
move_as_salesperson.action_post()
|
||||
|
||||
move_as_salesperson.sudo().action_post()
|
||||
|
||||
composer = self.env['account.move.send.wizard']\
|
||||
.with_user(self.sale_user2)\
|
||||
.with_context(active_model='account.move', active_ids=move_as_salesperson.ids)\
|
||||
.create({})
|
||||
|
||||
# Salesperson can send & print
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
composer.action_send_and_print()
|
||||
|
||||
@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 """
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
# -*- 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
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import freeze_time, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
|
||||
@freeze_time('2022-01-01')
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
||||
class TestAccruedSaleOrders(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.other_currency = cls.setup_other_currency('EUR')
|
||||
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
|
||||
|
|
@ -25,7 +29,7 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
'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.default_plan = cls.env['account.analytic.plan'].create({'name': 'Default'})
|
||||
cls.analytic_account_a = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_a',
|
||||
'plan_id': cls.default_plan.id,
|
||||
|
|
@ -36,11 +40,6 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
'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': [
|
||||
|
|
@ -48,9 +47,8 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
'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,
|
||||
'tax_ids': False,
|
||||
'analytic_distribution': {
|
||||
cls.analytic_account_a.id : 80.0,
|
||||
cls.analytic_account_b.id : 20.0,
|
||||
|
|
@ -60,16 +58,14 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
'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,
|
||||
'tax_ids': 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']
|
||||
|
|
@ -78,16 +74,23 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
'active_ids': cls.sale_order.ids,
|
||||
}).create({
|
||||
'account_id': cls.account_expense.id,
|
||||
'date': fields.Date.today(),
|
||||
})
|
||||
|
||||
def test_accrued_order(self):
|
||||
# self.wizard = self.wizard.with_context(accrual_entry_date=fields.Date.today())
|
||||
self.wizard.date = fields.Date.today()
|
||||
# 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, [
|
||||
# Call accrual wizard at today date because calling in the past will
|
||||
# re-compute delivred and invoiced quantities for this date and thus
|
||||
# generate nothing since there was no delivered quantity at this time.
|
||||
account_move = self.env['account.move'].search(self.wizard.create_entries()['domain'])
|
||||
self.assertRecordValues(account_move.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},
|
||||
|
|
@ -99,9 +102,9 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
])
|
||||
|
||||
# delivered products invoiced, nothing to invoice left
|
||||
self.sale_order.with_context(default_invoice_date=self.wizard.date)._create_invoices().action_post()
|
||||
invoices = self.sale_order._create_invoices()
|
||||
invoices.action_post()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.move.line']._invalidate_cache()
|
||||
self.wizard.create_entries()
|
||||
self.assertTrue(self.wizard.display_amount)
|
||||
|
||||
|
|
@ -110,7 +113,7 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
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.sale_order.currency_id = self.other_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},
|
||||
|
|
@ -127,12 +130,42 @@ class TestAccruedSaleOrders(AccountTestInvoicingCommon):
|
|||
|
||||
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}},
|
||||
{'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}},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 2000.0, 'credit': 0.0, 'analytic_distribution': {str(self.analytic_account_b.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}},
|
||||
# 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}},
|
||||
{'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}},
|
||||
{'account_id': self.alt_inc_account.id, 'debit': 0.0, 'credit': 2000.0, 'analytic_distribution': {str(self.analytic_account_b.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}},
|
||||
|
||||
])
|
||||
|
||||
def test_product_name_in_accrued_revenue_entry(self):
|
||||
self.sale_order.order_line.qty_delivered = 5
|
||||
|
||||
so_context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': self.sale_order.ids,
|
||||
'active_id': self.sale_order.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
}
|
||||
payment_params = {
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 50.0,
|
||||
}
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params)
|
||||
invoice = downpayment._create_invoices(self.sale_order)
|
||||
invoice.invoice_date = self.wizard.date
|
||||
invoice.action_post()
|
||||
self.wizard.create_entries()
|
||||
self.assertFalse(self.wizard.display_amount)
|
||||
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},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
|
@ -17,7 +16,7 @@ class TestSaleCommon(SaleCommon):
|
|||
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.currency_id.name, self.currency.name)
|
||||
self.assertEqual(self.empty_order.team_id, self.sale_team)
|
||||
self.assertEqual(self.empty_order.state, 'draft')
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ class TestSaleCommon(SaleCommon):
|
|||
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.currency_id.name, self.currency.name)
|
||||
self.assertEqual(self.sale_order.team_id, self.sale_team)
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
|
||||
|
|
@ -33,16 +32,14 @@ class TestSaleCommon(SaleCommon):
|
|||
|
||||
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.product_uom_id, 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.product_uom_id, self.uom_unit)
|
||||
self.assertEqual(service_line.price_total, 12.5 * 50)
|
||||
|
||||
self.assertEqual(self.sale_order.amount_total, 725.0)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
# -*- 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.base.tests.common import HttpCaseWithUserPortal
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccessRightsControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
||||
class TestAccessRightsControllers(HttpCase, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_portal = cls._create_new_portal_user()
|
||||
|
||||
@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)
|
||||
# set portal as customer to give portal access
|
||||
portal_so.partner_id = self.user_portal.partner_id.id
|
||||
|
||||
portal_so._portal_ensure_token()
|
||||
token = portal_so.access_token
|
||||
|
|
@ -66,7 +73,14 @@ class TestAccessRightsControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
|||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSalesControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
||||
class TestSalesControllers(HttpCase, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_portal = cls._create_new_portal_user()
|
||||
|
||||
def test_sales_portal_report(self):
|
||||
portal_so = self.sale_order.copy()
|
||||
portal_so.message_subscribe(self.user_portal.partner_id.ids)
|
||||
|
|
@ -75,14 +89,14 @@ class TestSalesControllers(BaseUsersCommon, HttpCase, SaleCommon):
|
|||
|
||||
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")
|
||||
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")
|
||||
self.assertEqual(req.headers['content-disposition'], f"attachment; filename*=UTF-8''Quotation_{portal_so.name}.pdf")
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
@tagged('post_install', '-at_install', 'mail_flow')
|
||||
class TestSaleSignature(HttpCaseWithUserPortal):
|
||||
|
||||
def test_01_portal_sale_signature_tour(self):
|
||||
|
|
@ -100,11 +114,17 @@ class TestSaleSignature(HttpCaseWithUserPortal):
|
|||
'order_id': sales_order.id,
|
||||
'product_id': self.env['product.product'].create({'name': 'A product'}).id,
|
||||
})
|
||||
self.assertFalse(sales_order.message_partner_ids)
|
||||
|
||||
# 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'))
|
||||
sales_order.with_context(**email_ctx).message_post_with_source(
|
||||
self.env['mail.template'].browse(email_ctx.get('default_template_id')),
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
self.assertFalse(
|
||||
sales_order.message_partner_ids,
|
||||
'Do not automatically set customer as follower, will be suggested recipient')
|
||||
|
||||
self.start_tour("/", 'sale_signature', login="portal")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo import Command
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import Form, tagged, users
|
||||
|
||||
from .common import TestSaleCommon
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
|
|
@ -31,6 +32,8 @@ class TestSaleOrderCreditLimit(TestSaleCommon):
|
|||
'currency_id': buck_currency.id,
|
||||
})
|
||||
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
cls.sales_user = cls.company_data['default_user_salesman']
|
||||
cls.sales_user.write({
|
||||
'login': "notaccountman",
|
||||
|
|
@ -41,9 +44,114 @@ class TestSaleOrderCreditLimit(TestSaleCommon):
|
|||
'partner_id': cls.partner_a.id,
|
||||
})
|
||||
|
||||
def test_credit_limit_multi_company(self):
|
||||
# multi-company setup
|
||||
company2 = self.company_data_2['company']
|
||||
|
||||
# Activate the Credit Limit feature
|
||||
company2.account_use_credit_limit = True
|
||||
|
||||
# Create and confirm a SO for that company
|
||||
sale_order = company2.env['sale.order'].create({
|
||||
'company_id': company2.id,
|
||||
'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': self.company_data_2['product_order_no'].id,
|
||||
'price_unit': 1000.0,
|
||||
})],
|
||||
})
|
||||
|
||||
self.assertEqual(self.partner_a.with_company(company2).credit_to_invoice, 0.0)
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertEqual(self.partner_a.with_company(company2).credit_to_invoice, 1000.0)
|
||||
partner_a_multi_company = self.partner_a.with_context(allowed_company_ids=[self.env.company.id, company2.id])
|
||||
self.assertEqual(partner_a_multi_company.credit_to_invoice, 0.0)
|
||||
self.assertEqual(self.partner_a.credit_to_invoice, 0.0)
|
||||
|
||||
def test_warning_on_invoice_with_downpayment(self):
|
||||
# Activate the Credit Limit feature and set a value for partner_a.
|
||||
self.env.company.account_use_credit_limit = True
|
||||
self.partner_a.credit_limit = 1000.0
|
||||
|
||||
# Create and confirm a SO to reach (but not exceed) partner_a's credit limit.
|
||||
sale_order = self.empty_order
|
||||
sale_order.write({
|
||||
'order_line': [Command.create({
|
||||
'name': self.company_data['product_order_no'].name,
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 1000.0,
|
||||
'tax_ids': False,
|
||||
})]
|
||||
})
|
||||
|
||||
# Check that partner_a's credit and credit_to_invoice is 0.0.
|
||||
self.assertEqual(self.partner_a.credit, 0.0)
|
||||
self.assertEqual(self.partner_a.credit_to_invoice, 0.0)
|
||||
|
||||
# Make sure partner_a's credit_to_invoice includes the newly confirmed SO.
|
||||
sale_order.action_confirm()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertEqual(self.partner_a.credit, 0.0)
|
||||
self.assertEqual(self.partner_a.credit_to_invoice, 1000.0)
|
||||
|
||||
# Create a 50% down payment invoice.
|
||||
self.env['sale.advance.payment.inv'].with_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,
|
||||
}).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 50,
|
||||
}).create_invoices()
|
||||
|
||||
invoice = sale_order.invoice_ids
|
||||
|
||||
# Check that the warning does not appear even though we are creating an invoice
|
||||
# that should bring partner_a's credit above its limit.
|
||||
self.assertEqual(invoice.partner_credit_warning, '')
|
||||
|
||||
|
||||
# Make the down payment invoice amount larger than the Amount to Invoice
|
||||
# and check that the warning appears with the correct amounts,
|
||||
# i.e. 1.500 instead of 2.500 (1.000 SO + 1.500 down payment invoice).
|
||||
invoice.invoice_line_ids.quantity = 3
|
||||
self.assertEqual(
|
||||
invoice.partner_credit_warning,
|
||||
"partner_a has reached its credit limit of: $\xa01,000.00\n"
|
||||
"Total amount due (including this document): $\xa01,500.00"
|
||||
)
|
||||
|
||||
invoice.invoice_line_ids.quantity = 1
|
||||
invoice.action_post()
|
||||
|
||||
# Create a credit note reversing the invoice
|
||||
self.env['account.move.reversal'].with_company(self.env.company).with_context(
|
||||
active_model="account.move",
|
||||
active_ids=invoice.ids,
|
||||
).create({
|
||||
'journal_id': invoice.journal_id.id,
|
||||
}).reverse_moves()
|
||||
|
||||
credit_note = sale_order.invoice_ids[1]
|
||||
credit_note.action_post()
|
||||
|
||||
# Check that the credit note is accounted for correctly for the amount_to_invoice
|
||||
self.assertEqual(sale_order.amount_to_invoice, sale_order.amount_total)
|
||||
|
||||
def test_credit_limit_multicurrency(self):
|
||||
self.partner_a.credit_limit = 50
|
||||
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 0.0,
|
||||
}])
|
||||
|
||||
order = self.empty_order
|
||||
order.write({
|
||||
'pricelist_id': self.buck_pricelist.id,
|
||||
|
|
@ -52,7 +160,7 @@ class TestSaleOrderCreditLimit(TestSaleCommon):
|
|||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 45.0,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
|
@ -65,37 +173,221 @@ class TestSaleOrderCreditLimit(TestSaleCommon):
|
|||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 65.0,
|
||||
'tax_id': False,
|
||||
'tax_ids': 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"
|
||||
"partner_a has reached its credit limit of: $\xa050.00\n"
|
||||
"Total amount due (including this document): $\xa055.00"
|
||||
)
|
||||
|
||||
# Make sure partner_a's credit_to_invoice includes the newly confirmed SO in the correct currency
|
||||
order.action_confirm()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 55.0,
|
||||
}])
|
||||
|
||||
# Make sure the invoice amount is converted correctly for the warning
|
||||
invoice = order._create_invoices(final=True)
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertEqual(
|
||||
invoice.partner_credit_warning,
|
||||
"partner_a has reached its credit limit of: $\xa050.00\n"
|
||||
"Total amount due (including this document): $\xa055.00"
|
||||
)
|
||||
|
||||
# Make sure the invoice amount is converted correctly for the partner.credit computation
|
||||
invoice.action_post()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 55.0,
|
||||
'credit_to_invoice': 0.0,
|
||||
}])
|
||||
|
||||
def test_invoice_independent_of_credit_to_invoice(self):
|
||||
# Activate the Credit Limit feature and set a value for partner_a.
|
||||
self.env.company.account_use_credit_limit = True
|
||||
self.partner_a.credit_limit = 1000.0
|
||||
|
||||
# Create and confirm a SO to reach (but not exceed) partner_a's credit limit.
|
||||
sale_order = self.empty_order
|
||||
sale_order.write({
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'price_unit': 1000.0,
|
||||
})]
|
||||
})
|
||||
|
||||
# Check that partner_a's credit and credit_to_invoice is 0.0.
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 0.0,
|
||||
}])
|
||||
|
||||
# Make sure partner_a's credit_to_invoice includes the newly confirmed SO.
|
||||
sale_order.action_confirm()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 1000.0,
|
||||
}])
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'test line',
|
||||
'quantity': 1,
|
||||
'price_unit': 100.0, # <= 1000 (sales order amount_total)
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 1000.0,
|
||||
}])
|
||||
|
||||
self.assertEqual(
|
||||
invoice.partner_credit_warning,
|
||||
"partner_a has reached its credit limit of: $\xa01,000.00\n"
|
||||
"Total amount due (including sales orders and this document): $\xa01,100.00"
|
||||
)
|
||||
|
||||
invoice.invoice_line_ids[0].price_unit = 2000 # > 1000 (sales order amount_total)
|
||||
self.assertEqual(
|
||||
invoice.partner_credit_warning,
|
||||
"partner_a has reached its credit limit of: $\xa01,000.00\n"
|
||||
"Total amount due (including sales orders and this document): $\xa03,000.00"
|
||||
)
|
||||
|
||||
invoice.action_post()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 2000.0,
|
||||
'credit_to_invoice': 1000.0,
|
||||
}])
|
||||
|
||||
def test_credit_limit_and_warning_overinvoiced_sales_order(self):
|
||||
# Activate the Credit Limit feature and set a value for partner_a.
|
||||
self.env.company.account_use_credit_limit = True
|
||||
self.partner_a.credit_limit = 1000.0
|
||||
|
||||
# Create 2 SOs
|
||||
self.empty_order.write({
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'price_unit': 1000.0,
|
||||
})]
|
||||
})
|
||||
sale_orders = self.empty_order + self.empty_order.copy()
|
||||
|
||||
# Check that partner_a's credit and credit_to_invoice is 0.0.
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 0.0,
|
||||
}])
|
||||
|
||||
for order in sale_orders:
|
||||
order.action_confirm()
|
||||
|
||||
# Make sure partner_a's credit_to_invoice includes the newly confirmed SOs.
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 0.0,
|
||||
'credit_to_invoice': 2000.0,
|
||||
}])
|
||||
|
||||
# Invoice 1 of the SOs.
|
||||
sale_order = sale_orders[0]
|
||||
self.assertEqual(sale_order.amount_to_invoice, 1000.0)
|
||||
invoice = sale_order._create_invoices(final=True)
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertEqual(invoice.amount_total, 1000.0)
|
||||
# Modify the amount of the invoice to be greater than the amount of the (single) SO.
|
||||
invoice.invoice_line_ids[0].price_unit = 2000.0
|
||||
|
||||
# Confirming the invoice will reduce the credit_to_invoice by 1000.
|
||||
# This is since the amount of the sales order it originates from is 1000 and
|
||||
# the amount of the invoice is more than 1000.
|
||||
self.assertEqual(
|
||||
invoice.partner_credit_warning,
|
||||
"partner_a has reached its credit limit of: $\xa01,000.00\n"
|
||||
"Total amount due (including sales orders and this document): $\xa03,000.00"
|
||||
)
|
||||
|
||||
# Check that confirming the invoice changes the credit amounts as described above.
|
||||
invoice.action_post()
|
||||
self.partner_a.invalidate_recordset(['credit', 'credit_to_invoice'])
|
||||
self.assertRecordValues(self.partner_a, [{
|
||||
'credit': 2000.0,
|
||||
'credit_to_invoice': 1000.0,
|
||||
}])
|
||||
|
||||
@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
|
||||
self.empty_order.user_id = self.env.user # it's our order now
|
||||
order = self.empty_order.with_env(self.env)
|
||||
|
||||
for group in self.partner_a._fields['credit'].groups.split(','):
|
||||
self.assertFalse(self.env.user.has_group(group))
|
||||
# Ensure we don't have access to accounting fields
|
||||
with self.assertRaises(AccessError, msg="We shouldn't have access to credit"):
|
||||
order.partner_id.credit_limit = 1e12
|
||||
order.sudo().partner_id.credit_limit = self.product_a.list_price
|
||||
|
||||
with Form(self.empty_order.with_env(self.env)) as order_form:
|
||||
with Form(order) as order_form:
|
||||
with order_form.order_line.new() as sol:
|
||||
sol.product_id = self.product_a
|
||||
sol.tax_id.clear()
|
||||
sol.tax_ids.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)
|
||||
sol.tax_ids.add(self.tax_sale_a)
|
||||
self.assertTrue(
|
||||
order_form.partner_credit_warning,
|
||||
"Credit warning should be displayed",
|
||||
)
|
||||
|
||||
def test_commercial_partner_credit(self):
|
||||
"""Ensure that credit to invoice gets computed on partners' companies."""
|
||||
company_a = self.env['res.partner'].create({
|
||||
'name': "Company A",
|
||||
'is_company': True,
|
||||
'credit_limit': 10000.0,
|
||||
'child_ids': [
|
||||
Command.link(self.partner_a.id),
|
||||
Command.create({
|
||||
'name': "Company A Invoice",
|
||||
'type': 'invoice',
|
||||
}),
|
||||
],
|
||||
})
|
||||
invoice_partner = company_a.child_ids.filtered(lambda p: p.type == 'invoice')
|
||||
|
||||
order = self.empty_order
|
||||
order.order_line = [Command.create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'price_unit': 600.0,
|
||||
'tax_ids': False,
|
||||
})]
|
||||
orders = order + order.copy({'partner_invoice_id': invoice_partner.id})
|
||||
orders.action_confirm()
|
||||
|
||||
self.assertFalse(
|
||||
self.partner_a.credit_to_invoice,
|
||||
"Credit should only apply to the commercial entity",
|
||||
)
|
||||
self.assertFalse(company_a.credit)
|
||||
self.assertEqual(company_a.credit_to_invoice, 1200.0)
|
||||
|
||||
invoices = orders._create_invoices()
|
||||
invoices.action_post()
|
||||
company_a.invalidate_recordset()
|
||||
self.assertFalse(company_a.credit_to_invoice)
|
||||
self.assertEqual(company_a.credit, 1200.0)
|
||||
|
|
|
|||
61
odoo-bringout-oca-ocb-sale/sale/tests/test_import_files.py
Normal file
61
odoo-bringout-oca-ocb-sale/sale/tests/test_import_files.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import unittest
|
||||
|
||||
from odoo.tests import TransactionCase, can_import, loaded_demo_data, tagged
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestImportFiles(TransactionCase):
|
||||
|
||||
@unittest.skipUnless(
|
||||
can_import("xlrd.xlsx") or can_import("openpyxl"), "XLRD/XLSX not available",
|
||||
)
|
||||
def test_import_quotation_template_xls(self):
|
||||
if not loaded_demo_data(self.env):
|
||||
self.skipTest('Needs demo data to be able to import those files')
|
||||
model = "sale.order"
|
||||
filename = "quotations_import_template.xlsx"
|
||||
|
||||
file_content = file_open(f"sale/static/xls/{filename}", "rb").read()
|
||||
import_wizard = self.env["base_import.import"].create(
|
||||
{
|
||||
"res_model": model,
|
||||
"file": file_content,
|
||||
"file_type": "application/vnd.ms-excel",
|
||||
},
|
||||
)
|
||||
|
||||
result = import_wizard.parse_preview(
|
||||
{
|
||||
"has_headers": True,
|
||||
},
|
||||
)
|
||||
self.assertIsNone(result.get("error"))
|
||||
field_names = ['/'.join(v) for v in result["matches"].values()]
|
||||
results = import_wizard.execute_import(
|
||||
field_names,
|
||||
[r.lower() for r in result["headers"]],
|
||||
{
|
||||
"import_skip_records": [],
|
||||
"import_set_empty_fields": [],
|
||||
"fallback_values": {},
|
||||
"name_create_enabled_fields": {},
|
||||
"encoding": "",
|
||||
"separator": "",
|
||||
"quoting": '"',
|
||||
"date_format": "",
|
||||
"datetime_format": "",
|
||||
"float_thousand_separator": ",",
|
||||
"float_decimal_separator": ".",
|
||||
"advanced": True,
|
||||
"has_headers": True,
|
||||
"keep_matches": False,
|
||||
"limit": 2000,
|
||||
"skip": 0,
|
||||
"tracking_disable": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(
|
||||
results["messages"],
|
||||
"results should be empty on successful import of ",
|
||||
)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestIrConfigParameter(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.IrConfigParameter = cls.env['ir.config_parameter']
|
||||
|
||||
# Create a test cron with XMLID.
|
||||
cls.test_cron = cls.env['ir.cron'].create({
|
||||
'name': "Test Cron",
|
||||
'model_id': cls.env.ref('sale.model_sale_order').id, # whatever
|
||||
'state': 'code',
|
||||
'code': '',
|
||||
'user_id': cls.env.uid,
|
||||
'interval_number': 1,
|
||||
'interval_type': 'days',
|
||||
'active': False,
|
||||
})
|
||||
cls.env['ir.model.data'].create({
|
||||
'name': 'test_cron',
|
||||
'model': 'ir.cron',
|
||||
'module': 'sale',
|
||||
'res_id': cls.test_cron.id,
|
||||
})
|
||||
|
||||
# Patch `_get_param_cron_mapping` to include the test records.
|
||||
original_get_param_cron_mapping = cls.IrConfigParameter._get_param_cron_mapping
|
||||
|
||||
def patched_get_param_cron_mapping(_self):
|
||||
mapping = original_get_param_cron_mapping()
|
||||
mapping['sale.test_param'] = 'sale.test_cron'
|
||||
return mapping
|
||||
|
||||
_get_param_cron_mapping_patcher = patch(
|
||||
'odoo.addons.sale.models.ir_config_parameter.IrConfigParameter._get_param_cron_mapping',
|
||||
new=patched_get_param_cron_mapping,
|
||||
)
|
||||
cls.startClassPatcher(_get_param_cron_mapping_patcher)
|
||||
|
||||
def test_creating_enabled_param_activates_cron(self):
|
||||
"""Test cron synchronization when creating an enabled config parameter."""
|
||||
self.assertFalse(self.test_cron.active)
|
||||
self.IrConfigParameter.create({'key': 'sale.test_param', 'value': 'True'})
|
||||
self.assertTrue(self.test_cron.active)
|
||||
|
||||
def test_creating_disabled_param_disables_cron(self):
|
||||
"""Test cron synchronization when creating a disabled config parameter."""
|
||||
self.test_cron.active = True
|
||||
self.IrConfigParameter.create({'key': 'sale.test_param', 'value': 'False'})
|
||||
self.assertFalse(self.test_cron.active)
|
||||
|
||||
def test_setting_enabled_param_value_activates_cron(self):
|
||||
"""Test cron synchronization when updating to an enabled config parameter."""
|
||||
param = self.IrConfigParameter.create({'key': 'sale.test_param', 'value': 'False'})
|
||||
param.value = 'True'
|
||||
self.assertTrue(self.test_cron.active)
|
||||
|
||||
def test_setting_disabled_param_value_disables_cron(self):
|
||||
"""Test cron synchronization when updating to a disabled config parameter."""
|
||||
param = self.IrConfigParameter.create({'key': 'sale.test_param', 'value': 'True'})
|
||||
param.value = 'False'
|
||||
self.assertFalse(self.test_cron.active)
|
||||
|
||||
def test_deleting_param_disables_cron(self):
|
||||
"""Test cron synchronization when deleting a config parameter."""
|
||||
param = self.IrConfigParameter.create({'key': 'sale.test_param', 'value': 'True'})
|
||||
param.unlink()
|
||||
self.assertFalse(self.test_cron.active)
|
||||
|
||||
def test_non_mapped_param_has_no_effect_on_cron(self):
|
||||
"""Test that non-mapped parameter don't affect crons."""
|
||||
self.IrConfigParameter.create({'key': 'sale.non_mapped_param', 'value': 'True'})
|
||||
self.assertFalse(self.test_cron.active)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
|
@ -7,68 +6,6 @@ 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`.)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
# 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 unittest.mock import patch
|
||||
|
||||
from odoo.tests import JsonRpcException, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
from odoo.addons.http_routing.tests.common import MockRequest
|
||||
from odoo.addons.mail.tests.common import MailCase
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
||||
class TestSalePayment(AccountPaymentCommon, MailCase, PaymentHttpCommon, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -21,174 +24,115 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
|||
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
|
||||
cls.provider.journal_id.inbound_payment_method_line_ids.filtered(
|
||||
lambda l: l.payment_provider_id == cls.provider
|
||||
).payment_account_id = cls.inbound_payment_method_line.payment_account_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)
|
||||
cls.sale_order.require_payment = True
|
||||
|
||||
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'],
|
||||
@mute_logger('odoo.http', 'werkzeug')
|
||||
def test_payment_amount_must_not_be_less_than_prepayment_amount(self):
|
||||
""" Test that accessing the portal page with a payment amount below prepayment amount raises
|
||||
an error. """
|
||||
res = self._make_http_get_request(f'/my/orders/{self.sale_order.id}', params={
|
||||
'access_token': self.sale_order._portal_ensure_token(), 'payment_amount': 1
|
||||
})
|
||||
self.assertEqual(res.status_code, 404)
|
||||
|
||||
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'])
|
||||
def test_is_down_payment_when_prepayment_amount_is_less_than_order_total(self):
|
||||
"""Test that we are in the downpayment case when the prepayment amount is less than the
|
||||
order total."""
|
||||
self.sale_order.prepayment_percent = 0.5
|
||||
self.assertTrue(CustomerPortal()._determine_is_down_payment(
|
||||
self.sale_order, 'whatever', None
|
||||
))
|
||||
|
||||
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)
|
||||
def test_is_not_down_payment_when_prepayment_amount_equals_order_total(self):
|
||||
"""Test that we are not in the downpayment case when the prepayment amount equals the order
|
||||
total."""
|
||||
self.sale_order.prepayment_percent = 1.0
|
||||
self.assertFalse(CustomerPortal()._determine_is_down_payment(
|
||||
self.sale_order, 'whatever', None
|
||||
))
|
||||
|
||||
# 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_is_down_payment_when_link_amount_is_less_than_order_total(self):
|
||||
"""Test that we are in the downpayment case when the link amount is less than the order
|
||||
total."""
|
||||
self.assertTrue(CustomerPortal()._determine_is_down_payment(
|
||||
self.sale_order, 'whatever', self.sale_order.amount_total * 0.5
|
||||
))
|
||||
|
||||
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
|
||||
def test_is_not_down_payment_when_link_amount_equals_order_total(self):
|
||||
"""Test that we are not in the downpayment case when the link amount equals the order total.
|
||||
"""
|
||||
self.assertFalse(CustomerPortal()._determine_is_down_payment(
|
||||
self.sale_order, 'whatever', self.sale_order.amount_total
|
||||
))
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
|
||||
def test_downpayment_amount_equals_link_amount_when_higher_than_prepayment_amount(self):
|
||||
"""Test that the payment link's amount is used for the transaction when that amount is
|
||||
higher than the prepayment amount and the user chose to pay a down payment."""
|
||||
self.sale_order.prepayment_percent = 0.5 # This should be ignored when the link is higher.
|
||||
link_amount = self.sale_order.amount_total * 0.7
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order, is_down_payment=True, payment_amount=link_amount
|
||||
)
|
||||
self.assertEqual(tx_values['amount'], link_amount)
|
||||
|
||||
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
|
||||
def test_downpayment_amount_equals_prepayment_amount_when_less_than_order_total(self):
|
||||
"""Test that the payment link's amount is used for the transaction when that amount is
|
||||
higher than the prepayment amount and the user chose to pay a down payment."""
|
||||
self.sale_order.prepayment_percent = 0.5
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order, is_down_payment=True, payment_amount=self.sale_order.amount_total
|
||||
)
|
||||
self.assertEqual(tx_values['amount'], self.sale_order._get_prepayment_required_amount())
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
def test_downpayment_amount_equals_prepayment_amount_when_no_link_amount(self):
|
||||
"""Test that the prepayment amount is used for the transaction when no payment amount is
|
||||
specified in the link and the user chose to pay a down payment."""
|
||||
self.sale_order.prepayment_percent = 0.5
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order, is_down_payment=True, payment_amount=None
|
||||
)
|
||||
prepayment_amount = self.sale_order._get_prepayment_required_amount()
|
||||
self.assertEqual(tx_values['amount'], prepayment_amount)
|
||||
|
||||
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)
|
||||
def test_payment_amount_equals_link_amount_when_order_is_confirmed(self):
|
||||
"""Test that the payment link's amount is used for the transaction when the order is
|
||||
confirmed."""
|
||||
self.sale_order.action_confirm()
|
||||
payment_amount = self.sale_order.amount_total * 0.5
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order, is_down_payment=False, payment_amount=payment_amount
|
||||
)
|
||||
self.assertEqual(tx_values['amount'], payment_amount)
|
||||
|
||||
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'])
|
||||
def test_payment_amount_equals_order_total_when_no_link_amount_and_order_is_confirmed(self):
|
||||
"""Test that the order total is used for the transaction when no payment amount is specified
|
||||
in the link and the order is confirmed."""
|
||||
self.sale_order.action_confirm()
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order, is_down_payment=False, payment_amount=None
|
||||
)
|
||||
self.assertEqual(tx_values['amount'], self.sale_order.amount_total)
|
||||
|
||||
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_full_amount_equals_order_total(self):
|
||||
"""Test that the order total is used for the transaction when the user chose to pay the full
|
||||
amount. """
|
||||
self.sale_order.prepayment_percent = 0.5 # This should not impact the 'full amount' option.
|
||||
with MockRequest(self.env):
|
||||
tx_values = CustomerPortal()._get_payment_values(
|
||||
self.sale_order,
|
||||
is_down_payment=False,
|
||||
payment_amount=self.sale_order._get_prepayment_required_amount()
|
||||
)
|
||||
self.assertEqual(tx_values['amount'], self.sale_order.amount_total)
|
||||
|
||||
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
|
||||
|
|
@ -208,37 +152,126 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
|||
reference='Test Transaction Draft 2',
|
||||
)
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
tx._reconcile_after_done()
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
|
||||
def test_auto_confirm_and_auto_invoice(self):
|
||||
"""
|
||||
Assuming that the automatic invoice setting is activated, we expect
|
||||
that after the payment is post processed:
|
||||
- invoice created
|
||||
- SO confirmed
|
||||
- Two emails sent: SO confirmation and default invoice email template
|
||||
"""
|
||||
# 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
|
||||
self.partner.email = 'customer@example.com' # make sure partner on SO has email set
|
||||
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()
|
||||
with (
|
||||
mute_logger('odoo.addons.sale.models.payment_transaction'),
|
||||
self.mock_mail_gateway(),
|
||||
):
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertEqual(len(self._new_mails), 2)
|
||||
self.assertTrue(self._new_mails.filtered(lambda x: 'Invoice' in x.subject))
|
||||
|
||||
def test_auto_confirm_and_auto_invoice_custom_mail_template(self):
|
||||
"""
|
||||
Assuming that the automatic invoice setting is activated and a custom
|
||||
email template for invoicing was selected, we expect that after the
|
||||
payment is post processed:
|
||||
- invoice created
|
||||
- SO confirmed
|
||||
- Two emails sent: SO confirmation and invoice email using the custom template
|
||||
"""
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
custom_template = self.env['mail.template'].create({
|
||||
'name': 'Custom Test Invoice Template',
|
||||
'model_id': self.env.ref('account.model_account_move').id,
|
||||
'subject': 'Your Custom Template',
|
||||
'partner_to': '{{ object.partner_id.id }}',
|
||||
'email_from': '{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}',
|
||||
})
|
||||
self.env['ir.config_parameter'].set_param('sale.default_invoice_email_template', custom_template.id)
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
self.partner.email = 'customer@example.com' # make sure partner on SO has email set
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
with (
|
||||
mute_logger('odoo.addons.sale.models.payment_transaction'),
|
||||
self.mock_mail_gateway(),
|
||||
):
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertEqual(len(self._new_mails), 2)
|
||||
self.assertTrue(self._new_mails.filtered(lambda x: 'Your Custom Template' in x.subject))
|
||||
|
||||
def test_auto_confirm_and_auto_invoice_custom_mail_template_unlinked(self):
|
||||
"""
|
||||
Assuming that the automatic invoice setting is activated and a custom
|
||||
email template for invoicing was selected. If the custom email template
|
||||
gets unlinked, the system parameter still stores the id, but code
|
||||
should fall back to default invoice email template. We expect that after the
|
||||
payment is post processed:
|
||||
- invoice created
|
||||
- SO confirmed
|
||||
- Two emails sent: SO confirmation and invoice email using the DEFAULT template
|
||||
"""
|
||||
# Set automatic invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
custom_template = self.env['mail.template'].create({
|
||||
'name': 'Custom Test Invoice Template',
|
||||
'model_id': self.env.ref('account.model_account_move').id,
|
||||
'subject': 'Your Custom Template',
|
||||
'partner_to': '{{ object.partner_id.id }}',
|
||||
'email_from': '{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}',
|
||||
})
|
||||
self.env['ir.config_parameter'].set_param('sale.default_invoice_email_template', custom_template.id)
|
||||
custom_template.unlink()
|
||||
|
||||
# Create the payment
|
||||
self.amount = self.sale_order.amount_total
|
||||
self.partner.email = 'customer@example.com' # make sure partner on SO has email set
|
||||
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
|
||||
with (
|
||||
mute_logger('odoo.addons.sale.models.payment_transaction'),
|
||||
self.mock_mail_gateway(),
|
||||
):
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertEqual(len(self._new_mails), 2)
|
||||
self.assertTrue(self._new_mails.filtered(lambda x: 'Invoice' in x.subject))
|
||||
|
||||
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')
|
||||
self.group_user.implied_ids += 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()
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'done')
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(self.sale_order.locked)
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertTrue(tx.invoice_ids.is_move_sent)
|
||||
|
|
@ -251,7 +284,7 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
|||
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()
|
||||
tx._post_process()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
self.assertFalse(tx.invoice_ids)
|
||||
|
|
@ -267,7 +300,241 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
|||
# 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()
|
||||
tx._post_process()
|
||||
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
|
||||
def test_invoice_is_final(self):
|
||||
"""Test that invoice generated from a payment are always final"""
|
||||
# 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'), patch(
|
||||
'odoo.addons.sale.models.sale_order.SaleOrder._create_invoices',
|
||||
return_value=self.env['account.move']
|
||||
) as _create_invoices_mock:
|
||||
tx._post_process()
|
||||
|
||||
self.assertTrue(_create_invoices_mock.call_args.kwargs['final'])
|
||||
|
||||
def test_linked_transactions_when_invoicing(self):
|
||||
self.provider.support_manual_capture = 'partial'
|
||||
partial_amount = self.sale_order.amount_total - 2
|
||||
|
||||
partial_tx_done = self._create_transaction(
|
||||
flow='direct',
|
||||
amount=partial_amount,
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='done',
|
||||
reference='partial_tx_done',
|
||||
)
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
partial_tx_done._post_process()
|
||||
partial_tx_pending = self._create_transaction(
|
||||
flow='direct',
|
||||
amount=2,
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='pending',
|
||||
reference='partial_tx_pending',
|
||||
)
|
||||
self.assertTrue(partial_tx_done.payment_id, msg="Account payment should have been created.")
|
||||
msg = "The created account payment shouldn't be reconciled as there are no invoice yet."
|
||||
self.assertFalse(partial_tx_pending.payment_id.is_reconciled, msg=msg)
|
||||
|
||||
# Add some noisy transactions
|
||||
self._create_transaction(
|
||||
flow='direct', sale_order_ids=[self.sale_order.id], state='draft', reference='draft_tx'
|
||||
)
|
||||
self._create_transaction(
|
||||
flow='direct', sale_order_ids=[self.sale_order.id], state='error', reference='error_tx'
|
||||
)
|
||||
self._create_transaction(
|
||||
flow='direct', sale_order_ids=[self.sale_order.id], state='cancel', reference='cncl_tx'
|
||||
)
|
||||
|
||||
msg = "The sale order should be linked to 5 transactions."
|
||||
self.assertEqual(len(self.sale_order.transaction_ids), 5, msg=msg)
|
||||
|
||||
self.sale_order.action_confirm()
|
||||
self.sale_order._create_invoices()
|
||||
|
||||
self.assertEqual(len(self.sale_order.invoice_ids), 1, msg="1 invoice should be created.")
|
||||
|
||||
first_invoice = self.sale_order.invoice_ids
|
||||
linked_txs = first_invoice.transaction_ids
|
||||
msg = "The newly created invoice should be linked to the done and pending transactions."
|
||||
self.assertEqual(len(linked_txs), 2, msg=msg)
|
||||
expected_linked_tx = (partial_tx_done, partial_tx_pending)
|
||||
self.assertTrue(all(tx in expected_linked_tx for tx in linked_txs), msg=msg)
|
||||
msg = "The payment shouldn't be reconciled yet."
|
||||
self.assertFalse(partial_tx_done.payment_id.is_reconciled, msg=msg)
|
||||
|
||||
partial_tx_done._post_process()
|
||||
|
||||
msg = "The payment should now be reconciled."
|
||||
self.assertTrue(partial_tx_done.payment_id.is_reconciled, msg=msg)
|
||||
|
||||
self.sale_order.order_line[0].product_uom_qty += 2
|
||||
self.sale_order._create_invoices()
|
||||
|
||||
second_invoice = self.sale_order.invoice_ids - first_invoice
|
||||
msg = "The newly created invoice should only be linked to the pending transaction."
|
||||
self.assertEqual(len(second_invoice.transaction_ids), 1, msg=msg)
|
||||
self.assertEqual(second_invoice.transaction_ids.state, 'pending', msg=msg)
|
||||
|
||||
def test_downpayment_confirm_sale_order_sufficient_amount(self):
|
||||
"""Paying down payments can confirm an order if amount is enough."""
|
||||
self.sale_order.prepayment_percent = 0.1
|
||||
order_amount = self.sale_order.amount_total
|
||||
|
||||
tx = self._create_transaction(
|
||||
flow='direct',
|
||||
amount=order_amount * self.sale_order.prepayment_percent,
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='done',
|
||||
)
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._post_process()
|
||||
|
||||
self.assertTrue(self.sale_order.state == 'sale')
|
||||
|
||||
def test_downpayment_automatic_invoice(self):
|
||||
"""
|
||||
Down payment invoices should be created when a down payment confirms
|
||||
the order and automatic invoice is checked.
|
||||
"""
|
||||
self.sale_order.prepayment_percent = 0.2
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
|
||||
tx = self._create_transaction(
|
||||
flow='direct',
|
||||
amount=self.sale_order.amount_total * self.sale_order.prepayment_percent,
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='done')
|
||||
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._post_process()
|
||||
|
||||
invoice = self.sale_order.invoice_ids
|
||||
self.assertTrue(len(invoice) == 1)
|
||||
self.assertTrue(invoice.line_ids[0].is_downpayment)
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_transaction_route_rejects_unexpected_kwarg(self):
|
||||
url = self._build_url(f'/my/orders/{self.sale_order.id}/transaction')
|
||||
route_kwargs = {
|
||||
'access_token': self.sale_order._portal_ensure_token(),
|
||||
'partner_id': self.partner.id, # This should be rejected.
|
||||
}
|
||||
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
|
||||
self.make_jsonrpc_request(url, route_kwargs)
|
||||
|
||||
def test_partial_payment_confirm_order(self):
|
||||
"""
|
||||
Test that a sale order can be confirmed through partial payments and that
|
||||
correct mails are sent each time.
|
||||
"""
|
||||
self.amount = self.sale_order.amount_total / 2
|
||||
|
||||
with patch(
|
||||
'odoo.addons.sale.models.sale_order.SaleOrder._send_order_notification_mail',
|
||||
) as notification_mail_mock:
|
||||
tx_pending = self._create_transaction(
|
||||
flow='direct',
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='pending',
|
||||
reference='Test Transaction Draft 1',
|
||||
)
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
|
||||
tx_pending._set_done()
|
||||
tx_pending._post_process()
|
||||
|
||||
self.assertEqual(notification_mail_mock.call_count, 1)
|
||||
notification_mail_mock.assert_called_once_with(
|
||||
self.env.ref('sale.mail_template_sale_payment_executed'))
|
||||
self.assertEqual(self.sale_order.state, 'draft')
|
||||
self.assertEqual(self.sale_order.amount_paid, self.amount)
|
||||
|
||||
tx_done = self._create_transaction(
|
||||
flow='direct',
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='done',
|
||||
reference='Test Transaction Draft 2',
|
||||
)
|
||||
tx_done._post_process()
|
||||
|
||||
self.assertEqual(notification_mail_mock.call_count, 2)
|
||||
order_confirmation_mail_template_id = int(
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("sale.default_confirmation_template", self.env.ref("sale.mail_template_sale_confirmation").id)
|
||||
)
|
||||
notification_mail_mock.assert_called_with(self.env["mail.template"].browse(order_confirmation_mail_template_id))
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
|
||||
def test_automatic_invoice_mail_author(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
|
||||
|
||||
portal_user = self.env['res.users'].create({
|
||||
'name': 'Portal Customer',
|
||||
'login': 'portal@test.com',
|
||||
'email': 'portal@test.com',
|
||||
'partner_id': self.partner_a.id,
|
||||
'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])],
|
||||
})
|
||||
portal_user.partner_id.invoice_sending_method = 'email'
|
||||
|
||||
sale_order = self.env['sale.order'].with_user(portal_user).sudo().create({
|
||||
'partner_id': portal_user.partner_id.id,
|
||||
'user_id': self.sale_user.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 100.0,
|
||||
})],
|
||||
})
|
||||
|
||||
self.amount = sale_order.amount_total
|
||||
tx = self._create_transaction(
|
||||
flow='redirect',
|
||||
sale_order_ids=[sale_order.id],
|
||||
state='done'
|
||||
)
|
||||
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx.with_user(portal_user).sudo()._post_process()
|
||||
|
||||
# Verify invoice was created and sent successfully
|
||||
invoice = sale_order.invoice_ids[0]
|
||||
self.assertTrue(invoice.is_move_sent, "Invoice should be marked as sent")
|
||||
invoice_sent_mail = invoice.message_ids[0]
|
||||
self.assertTrue(invoice_sent_mail.author_id.id not in invoice_sent_mail.notified_partner_ids.ids)
|
||||
|
||||
def test_refund_message_author_is_logged_in_user_for_sale_order(self):
|
||||
"""Ensure that the chatter message author is the user processing the refund."""
|
||||
self.provider.support_refund = 'full_only'
|
||||
|
||||
tx = self._create_transaction(
|
||||
'redirect',
|
||||
sale_order_ids=[self.sale_order.id],
|
||||
state='done',
|
||||
)
|
||||
tx._post_process()
|
||||
|
||||
with patch.object(
|
||||
self.env.registry['mail.thread'], 'message_post', autospec=True
|
||||
) as message_post_mock:
|
||||
tx.action_refund()
|
||||
author_id = message_post_mock.call_args[1].get("author_id")
|
||||
|
||||
self.assertEqual(author_id, self.user.partner_id.id)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProductAttributeValue(HttpCase, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.product_attribute = cls.env['product.attribute'].create({
|
||||
'name': 'PA',
|
||||
'sequence': 1,
|
||||
'create_variant': 'no_variant',
|
||||
'value_ids': [
|
||||
Command.create({'name': f'pa_value_{i + 1}', 'sequence': i})
|
||||
for i in range(3)
|
||||
]
|
||||
})
|
||||
cls.a1, cls.a2, cls.a3 = cls.product_attribute.value_ids
|
||||
cls.product_template, cls.archived_template = cls.env['product.template'].create([
|
||||
{
|
||||
'name': 'P1',
|
||||
'type': 'consu',
|
||||
'attribute_line_ids': [Command.create({
|
||||
'attribute_id': cls.product_attribute.id,
|
||||
'value_ids': [Command.set([cls.a1.id, cls.a3.id])],
|
||||
})],
|
||||
},
|
||||
{
|
||||
'name': 'P2',
|
||||
'type': 'consu',
|
||||
'attribute_line_ids': [Command.create({
|
||||
'attribute_id': cls.product_attribute.id,
|
||||
'value_ids': [Command.set([cls.a1.id, cls.a2.id])],
|
||||
})],
|
||||
}
|
||||
])
|
||||
cls.archived_template.action_archive()
|
||||
cls.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': cls.product_template.product_variant_id.id,
|
||||
'product_no_variant_attribute_value_ids': [
|
||||
Command.set(
|
||||
cls.product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id.id == cls.a3.id
|
||||
).ids,
|
||||
),
|
||||
],
|
||||
}),
|
||||
]
|
||||
cls.order_line = cls.empty_order.order_line
|
||||
|
||||
def test_attribute_values_deletion_or_archiving(self):
|
||||
"""Check that product attributes can be deleted if product or linked ptav are archived."""
|
||||
if self.env['ir.module.module']._get('sale_management').state != 'installed':
|
||||
self.skipTest("Sale App is not installed, Sale menu is not accessible.")
|
||||
|
||||
# Make sure variants are enabled (needed for menu access)
|
||||
group_variant = self.env.ref('product.group_product_variant')
|
||||
self.group_user.implied_ids = [Command.link(group_variant.id)]
|
||||
|
||||
self.product_template.attribute_line_ids.update({'value_ids': [Command.set([self.a1.id])]})
|
||||
self.assertEqual(
|
||||
self.order_line.product_no_variant_attribute_value_ids.product_attribute_value_id,
|
||||
self.a3,
|
||||
)
|
||||
self.assertFalse(self.order_line.product_no_variant_attribute_value_ids.ptav_active)
|
||||
self.start_tour("/odoo", 'delete_product_attribute_value_tour', login="admin")
|
||||
241
odoo-bringout-oca-ocb-sale/sale/tests/test_product_catalog.py
Normal file
241
odoo-bringout-oca-ocb-sale/sale/tests/test_product_catalog.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProductCatalog(HttpCase, SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.res_model = cls.empty_order._name
|
||||
cls.res_id = cls.empty_order.id
|
||||
cls.base_url = cls.base_url()
|
||||
cls.products = cls.product + cls.service_product
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.authenticate(self.sale_manager.login, self.sale_manager.login)
|
||||
|
||||
def request_get_order_lines_info(self, products, **kwargs):
|
||||
response = self.opener.post(
|
||||
url=self.base_url + '/product/catalog/order_lines_info',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.res_model,
|
||||
'order_id': self.res_id,
|
||||
'product_ids': products.ids,
|
||||
**kwargs,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.json()['result']
|
||||
|
||||
def request_update_order_line_info(self, product, quantity=1.0, **kwargs):
|
||||
response = self.opener.post(
|
||||
url=self.base_url + '/product/catalog/update_order_line_info',
|
||||
json={
|
||||
'params': {
|
||||
'res_model': self.res_model,
|
||||
'order_id': self.res_id,
|
||||
'product_id': product.id,
|
||||
'quantity': quantity,
|
||||
**kwargs,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.json()['result']
|
||||
|
||||
def _get_default_catalog_data(self, product):
|
||||
return {
|
||||
'quantity': 0,
|
||||
'readOnly': False,
|
||||
'productType': product.type,
|
||||
'price': product.lst_price,
|
||||
}
|
||||
|
||||
def check_catalog_data(self, products, expected_data=None):
|
||||
expected_data = expected_data or {}
|
||||
catalog_data = self.request_get_order_lines_info(products=products)
|
||||
for product in products:
|
||||
self.assertIn(str(product.id), catalog_data)
|
||||
product_expected_data = {
|
||||
**self._get_default_catalog_data(product),
|
||||
**expected_data.get(product.id, {}),
|
||||
}
|
||||
product_data = catalog_data[str(product.id)]
|
||||
for key, value in product_expected_data.items():
|
||||
self.assertEqual(
|
||||
product_data[key],
|
||||
value,
|
||||
)
|
||||
|
||||
def _create_pricelist_discount_rules(self):
|
||||
self.pricelist.item_ids = [
|
||||
Command.create({
|
||||
'min_quantity': 1.0,
|
||||
'product_id': self.product.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 50,
|
||||
}),
|
||||
Command.create({
|
||||
'min_quantity': 2.0,
|
||||
'product_id': self.service_product.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 50,
|
||||
})
|
||||
]
|
||||
|
||||
def test_catalog_context(self):
|
||||
action_data = self.empty_order.action_add_from_catalog()
|
||||
catalog_context = action_data['context']
|
||||
self.assertEqual(catalog_context['product_catalog_order_id'], self.empty_order.id)
|
||||
self.assertEqual(catalog_context['product_catalog_order_model'], self.res_model)
|
||||
self.assertEqual(
|
||||
catalog_context['product_catalog_currency_id'],
|
||||
self.empty_order.currency_id.id,
|
||||
)
|
||||
# Equal to false, as `price_unit` doesn't have a precision set.
|
||||
self.assertFalse(catalog_context['product_catalog_digits'])
|
||||
|
||||
def test_empty_order_data(self):
|
||||
self.check_catalog_data(self.products)
|
||||
|
||||
# TODO VFE in master, forbid updates when order is readonly
|
||||
def test_readonly_order_data(self):
|
||||
self.empty_order._action_cancel()
|
||||
|
||||
# Readonly order because in cancelled state
|
||||
self.check_catalog_data(
|
||||
self.service_product,
|
||||
{
|
||||
self.service_product.id: {'readOnly': True},
|
||||
}
|
||||
)
|
||||
|
||||
def test_data(self):
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': self.service_product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
]
|
||||
|
||||
self.check_catalog_data(
|
||||
self.products,
|
||||
{
|
||||
self.service_product.id: {'quantity': 1.0},
|
||||
}
|
||||
)
|
||||
|
||||
def test_data_with_pricelist_rules(self):
|
||||
self._create_pricelist_discount_rules()
|
||||
self.assertEqual(self.empty_order.pricelist_id, self.pricelist)
|
||||
self.check_catalog_data(
|
||||
self.products,
|
||||
{
|
||||
self.product.id: {'price': self.product.lst_price / 2},
|
||||
}
|
||||
)
|
||||
|
||||
def test_data_with_discounted_lines(self):
|
||||
self._create_pricelist_discount_rules()
|
||||
self.env['res.config.settings'].create({
|
||||
# Discounts included in price
|
||||
'group_product_pricelist': True,
|
||||
'group_discount_per_so_line': True,
|
||||
}).execute()
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
})
|
||||
]
|
||||
sol = self.empty_order.order_line
|
||||
self.assertEqual(sol.price_unit, self.product.lst_price)
|
||||
self.assertEqual(sol.discount, 50)
|
||||
|
||||
self.check_catalog_data(
|
||||
self.products,
|
||||
{
|
||||
self.product.id: {
|
||||
'quantity': 1.0,
|
||||
'price': self.product.lst_price / 2
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
self.assertFalse(self.empty_order.order_line)
|
||||
|
||||
# Add product to order
|
||||
product = self.service_product
|
||||
update_data = self.request_update_order_line_info(product=product)
|
||||
sol = self.empty_order.order_line
|
||||
self.assertEqual(sol.product_id, product)
|
||||
self.assertEqual(sol.product_uom_qty, 1.0)
|
||||
self.assertEqual(update_data, sol.price_unit)
|
||||
self.assertEqual(update_data, product.lst_price)
|
||||
|
||||
def test_update_with_pricelist_rules(self):
|
||||
self._create_pricelist_discount_rules()
|
||||
self.env['res.config.settings'].create({
|
||||
# Discounts included in price
|
||||
'group_product_pricelist': True,
|
||||
'group_discount_per_so_line': False,
|
||||
}).execute()
|
||||
|
||||
# Add first item --> no discount
|
||||
product = self.service_product
|
||||
update_data = self.request_update_order_line_info(product=product)
|
||||
sol = self.empty_order.order_line
|
||||
self.assertRecordValues(
|
||||
sol, [{
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': product.lst_price,
|
||||
'discount': 0.0,
|
||||
}]
|
||||
)
|
||||
self.assertEqual(update_data, product.lst_price)
|
||||
|
||||
# Add a second item --> should trigger the pricelist discount
|
||||
update_data = self.request_update_order_line_info(product=product, quantity=2.0)
|
||||
self.assertRecordValues(
|
||||
sol, [{
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'price_unit': product.lst_price / 2,
|
||||
'discount': 0.0,
|
||||
}]
|
||||
)
|
||||
self.assertEqual(update_data, product.lst_price / 2)
|
||||
|
||||
# Enable discounts, add item --> discount should be on discount field
|
||||
self.env['res.config.settings'].create({
|
||||
# Discounts included in price
|
||||
'group_product_pricelist': True,
|
||||
'group_discount_per_so_line': True,
|
||||
}).execute()
|
||||
update_data = self.request_update_order_line_info(product=product, quantity=3.0)
|
||||
self.assertRecordValues(
|
||||
sol, [{
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'price_unit': product.lst_price,
|
||||
'discount': 50.0,
|
||||
}]
|
||||
)
|
||||
self.assertEqual(update_data, product.lst_price / 2)
|
||||
|
||||
def test_remove_product_from_catalog_without_sol(self):
|
||||
"""Test that removing a product from the catalog right after clicking Add button"""
|
||||
product = self.service_product
|
||||
update_data = self.request_update_order_line_info(product=product, quantity=0.0)
|
||||
|
||||
self.assertEqual(update_data, product.lst_price)
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.addons.product.tests.common import ProductVariantsCommon
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProductConfiguratorData(HttpCaseWithUserDemo, ProductVariantsCommon, SaleCommon):
|
||||
|
||||
def request_get_values(self, product_template, ptav_ids=None):
|
||||
base_url = product_template.get_base_url()
|
||||
response = self.url_open(
|
||||
url=base_url + '/sale/product_configurator/get_values',
|
||||
json={
|
||||
'params': {
|
||||
'product_template_id': product_template.id,
|
||||
'quantity': 1.0,
|
||||
'currency_id': 1,
|
||||
'so_date': str(self.env.cr.now()),
|
||||
'product_uom_id': None,
|
||||
'company_id': None,
|
||||
'pricelist_id': None,
|
||||
'ptav_ids': ptav_ids,
|
||||
'only_main_product': False,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.json()['result']
|
||||
|
||||
def create_product_template_with_2_attributes(self):
|
||||
return self.env['product.template'].create({
|
||||
'name': 'Shirt',
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': self.size_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.size_attribute_l.id,
|
||||
self.size_attribute_m.id,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.color_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.color_attribute_red.id,
|
||||
self.color_attribute_blue.id,
|
||||
])
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def create_product_template_with_attribute_no_variant(self):
|
||||
return self.env['product.template'].create({
|
||||
'name': 'Chair',
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': self.no_variant_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.no_variant_attribute_extra.id
|
||||
]),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.color_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.color_attribute_red.id,
|
||||
self.color_attribute_blue.id,
|
||||
])
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def create_product_template_with_2_attribute_no_variant(self):
|
||||
return self.env['product.template'].create({
|
||||
'name': 'Chair',
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': self.no_variant_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.no_variant_attribute_extra.id,
|
||||
self.no_variant_attribute_second.id,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.color_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.color_attribute_red.id,
|
||||
self.color_attribute_blue.id,
|
||||
])
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_dropped_value_isnt_shown(self):
|
||||
self.assertEqual(len(self.product_template_sofa.product_variant_ids), 3)
|
||||
|
||||
# Use variants s.t. they are archived and not deleted when value is removed
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id
|
||||
})
|
||||
for product in self.product_template_sofa.product_variant_ids
|
||||
]
|
||||
self.empty_order.action_confirm()
|
||||
|
||||
# Remove attribute value
|
||||
self.product_template_sofa.attribute_line_ids.value_ids -= self.color_attribute_red
|
||||
self.assertEqual(len(self.product_template_sofa.product_variant_ids.filtered('active')), 2)
|
||||
|
||||
self.authenticate('demo', 'demo')
|
||||
result = self.request_get_values(self.product_template_sofa)
|
||||
|
||||
# Make sure the inactive ptav was removed from the loaded attributes
|
||||
self.assertEqual(len(result['products'][0]['attribute_lines'][0]['attribute_values']), 2)
|
||||
|
||||
def test_dropped_attribute(self):
|
||||
product_template = self.create_product_template_with_2_attributes()
|
||||
self.assertEqual(len(product_template.product_variant_ids), 4)
|
||||
|
||||
# Use variants s.t. they are archived and not deleted when value is removed
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id
|
||||
})
|
||||
for product in product_template.product_variant_ids
|
||||
]
|
||||
self.empty_order.action_confirm()
|
||||
|
||||
# Remove attribute
|
||||
product_template.attribute_line_ids[0].unlink()
|
||||
self.assertEqual(len(product_template.product_variant_ids), 2)
|
||||
|
||||
self.authenticate('demo', 'demo')
|
||||
result = self.request_get_values(product_template)
|
||||
|
||||
# Make sure archived combinations with inactive ptav are not loaded as it's useless to
|
||||
# exclude combinations that are not even available
|
||||
self.assertFalse(result['products'][0]['archived_combinations'])
|
||||
|
||||
def test_dropped_attribute_value(self):
|
||||
product_template = self.create_product_template_with_2_attributes()
|
||||
self.assertEqual(len(product_template.product_variant_ids), 4)
|
||||
|
||||
# Use variants s.t. they are archived and not deleted when value is removed
|
||||
self.empty_order.order_line = [
|
||||
Command.create(
|
||||
{
|
||||
'product_id': product.id
|
||||
}
|
||||
)
|
||||
for product in product_template.product_variant_ids
|
||||
]
|
||||
self.empty_order.action_confirm()
|
||||
|
||||
# Remove attribute value red
|
||||
product_template.attribute_line_ids.filtered(
|
||||
lambda ptal: ptal.attribute_id == self.color_attribute
|
||||
).value_ids = [Command.unlink(self.color_attribute_red.id)]
|
||||
self.assertEqual(len(product_template.product_variant_ids), 2)
|
||||
archived_variants = product_template.with_context(
|
||||
active_test=False
|
||||
).product_variant_ids - product_template.product_variant_ids
|
||||
self.assertEqual(len(archived_variants), 2)
|
||||
|
||||
archived_ptav = product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.color_attribute_red
|
||||
)
|
||||
# Choose the variant (red, L)
|
||||
variant_ptav_ids = [
|
||||
archived_ptav.id,
|
||||
product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.size_attribute_l
|
||||
).id,
|
||||
]
|
||||
self.authenticate('demo', 'demo')
|
||||
result = self.request_get_values(product_template, variant_ptav_ids)
|
||||
archived_ptav = archived_variants.product_template_attribute_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.color_attribute_red
|
||||
)
|
||||
|
||||
# When requested combination contains inactive ptav
|
||||
# check that archived combinations are loaded
|
||||
self.assertEqual(
|
||||
len(result['products'][0]['archived_combinations']),
|
||||
2
|
||||
)
|
||||
for combination in result['products'][0]['archived_combinations']:
|
||||
self.assertIn(archived_ptav.id, combination)
|
||||
|
||||
# When requested combination contains inactive ptav check that exclusions contains it
|
||||
self.assertIn(str(archived_ptav.id), result['products'][0]['exclusions'])
|
||||
|
||||
def test_excluded_inactive_ptav(self):
|
||||
product_template = self.create_product_template_with_2_attributes()
|
||||
self.assertEqual(len(product_template.product_variant_ids), 4)
|
||||
|
||||
ptav_with_exclusion = product_template.attribute_line_ids[0].product_template_value_ids[0]
|
||||
ptav_excluded = product_template.attribute_line_ids[1].product_template_value_ids[0]
|
||||
|
||||
# Add an exclusion
|
||||
ptav_with_exclusion.write({
|
||||
'exclude_for': [
|
||||
Command.create({
|
||||
'product_tmpl_id': product_template.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
ptav_excluded.id,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertEqual(len(product_template.product_variant_ids), 3)
|
||||
|
||||
self.authenticate('demo', 'demo')
|
||||
result = self.request_get_values(product_template)
|
||||
# The PTAVs should be mutually excluded
|
||||
self.assertEqual(result['products'][0]['exclusions']
|
||||
[str(ptav_with_exclusion.id)], [ptav_excluded.id])
|
||||
self.assertEqual(result['products'][0]['exclusions']
|
||||
[str(ptav_excluded.id)], [ptav_with_exclusion.id])
|
||||
|
||||
ptav_with_exclusion.write({'ptav_active': False})
|
||||
result = self.request_get_values(product_template)
|
||||
# The inactive PTAV should not be in the product exclusions dict
|
||||
self.assertFalse(str(ptav_with_exclusion.id) in result['products'][0]['exclusions'])
|
||||
# The inactive PTAV should not be in the exclusions of the excluded PTAV
|
||||
self.assertEqual(result['products'][0]['exclusions'][str(ptav_excluded.id)], [])
|
||||
|
||||
ptav_with_exclusion.write({'ptav_active': True})
|
||||
ptav_excluded.write({'ptav_active': False})
|
||||
result = self.request_get_values(product_template)
|
||||
# The excluded inactive PTAV should not be in the exclusions of the first PTAV
|
||||
self.assertEqual(result['products'][0]['exclusions'][str(ptav_with_exclusion.id)], [])
|
||||
# The excluded inactive PTAV should not be in the product exclusions dict
|
||||
self.assertFalse(str(ptav_excluded.id) in result['products'][0]['exclusions'])
|
||||
|
||||
ptav_with_exclusion.write({'ptav_active': False})
|
||||
ptav_excluded.write({'ptav_active': False})
|
||||
result = self.request_get_values(product_template)
|
||||
|
||||
# The inactive PTAVs should not be in the product exclusions dict
|
||||
self.assertFalse(str(ptav_with_exclusion.id) in result['products'][0]['exclusions'])
|
||||
self.assertFalse(str(ptav_excluded.id) in result['products'][0]['exclusions'])
|
||||
|
||||
def test_ptal_values_set_for_no_variant_atribute(self):
|
||||
'''
|
||||
Test that selected_attribute_value_id is set for attribute with only one variant and
|
||||
`create_variant`: `no_variant`.
|
||||
'''
|
||||
product_template = self.create_product_template_with_attribute_no_variant()
|
||||
|
||||
self.authenticate('demo', 'demo')
|
||||
|
||||
ptav_red = product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.color_attribute_red
|
||||
)
|
||||
result = self.request_get_values(product_template, [ptav_red.id])
|
||||
self.assertTrue(result['products'][0]['attribute_lines'][1]['selected_attribute_value_ids'])
|
||||
|
||||
def test_dropped_attribute_value_custom_no_variant(self):
|
||||
product_template = self.create_product_template_with_2_attribute_no_variant()
|
||||
|
||||
# Use variants s.t. they are archived and not deleted when value is removed
|
||||
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id,
|
||||
'product_no_variant_attribute_value_ids': product.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda p: p.attribute_id.create_variant == 'no_variant'
|
||||
),
|
||||
})
|
||||
for product in product_template.product_variant_ids]
|
||||
self.empty_order.action_confirm()
|
||||
|
||||
# Remove attribute value extra
|
||||
product_template.attribute_line_ids.filtered(
|
||||
lambda ptal: ptal.attribute_id == self.no_variant_attribute
|
||||
).value_ids = [Command.unlink(self.no_variant_attribute_extra.id)]
|
||||
|
||||
archived_ptav = product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.no_variant_attribute_extra
|
||||
)
|
||||
self.assertFalse(archived_ptav.ptav_active)
|
||||
self.assertEqual(
|
||||
product_template.attribute_line_ids.filtered(
|
||||
lambda ptal: ptal.attribute_id == self.no_variant_attribute
|
||||
).product_template_value_ids[0],
|
||||
archived_ptav,
|
||||
)
|
||||
# Choose the variant (red)
|
||||
variant_ptav_ids = [
|
||||
product_template.attribute_line_ids.product_template_value_ids.filtered(
|
||||
lambda ptav: ptav.product_attribute_value_id == self.color_attribute_red
|
||||
).id,
|
||||
]
|
||||
self.authenticate('demo', 'demo')
|
||||
result = self.request_get_values(product_template, variant_ptav_ids)
|
||||
selected_values = [
|
||||
selected_value
|
||||
for product in result['products'][0]['attribute_lines']
|
||||
for selected_value in product['selected_attribute_value_ids']]
|
||||
|
||||
# Make sure that deleted value is not selected
|
||||
self.assertNotIn(archived_ptav.id, selected_values)
|
||||
|
||||
def test_attribute_removal(self):
|
||||
product_template_2lines_2attributes = self.env['product.template'].create({
|
||||
'name': '2 lines 2 attributes',
|
||||
'uom_id': self.uom_unit.id,
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': self.color_attribute.id,
|
||||
'value_ids': [Command.set([
|
||||
self.color_attribute_red.id,
|
||||
self.color_attribute_blue.id,
|
||||
])],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.size_attribute.id,
|
||||
'value_ids': [Command.set([
|
||||
self.size_attribute_s.id,
|
||||
self.size_attribute_m.id,
|
||||
])]
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Sell all variants
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': product.id,
|
||||
})
|
||||
for product in product_template_2lines_2attributes.product_variant_ids
|
||||
]
|
||||
|
||||
def _get_ptavs():
|
||||
return product_template_2lines_2attributes.with_context(
|
||||
active_test=False
|
||||
).attribute_line_ids.product_template_value_ids
|
||||
|
||||
def _get_archived_variants():
|
||||
return product_template_2lines_2attributes.with_context(
|
||||
active_test=False
|
||||
).product_variant_ids.filtered(lambda p: not p.active)
|
||||
|
||||
def _get_active_variants():
|
||||
return product_template_2lines_2attributes.product_variant_ids
|
||||
|
||||
self.assertEqual(len(_get_ptavs()), 4)
|
||||
product_template_2lines_2attributes.attribute_line_ids = [
|
||||
Command.unlink(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 product_template_2lines_2attributes.product_variant_ids
|
||||
]
|
||||
|
||||
self.assertEqual(len(_get_archived_variants()), 4)
|
||||
self.assertEqual(len(_get_active_variants()), 2)
|
||||
|
||||
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 = 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()
|
||||
)
|
||||
)
|
||||
|
||||
def test_multiple_attribute_lines_same_attribute(self):
|
||||
"""
|
||||
Test that product configurator works correctly when multiple attribute
|
||||
lines reference the same attribute. This ensures no KeyError is raised
|
||||
when building the attrs_map in _get_product_information.
|
||||
"""
|
||||
# Create a product template with two attribute lines referencing the same attribute
|
||||
product_template = self.env['product.template'].create({
|
||||
'name': 'Multi Size Shirt',
|
||||
'categ_id': self.product_category.id,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': self.size_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.size_attribute_l.id,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.color_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.color_attribute_red.id,
|
||||
self.color_attribute_blue.id,
|
||||
])
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': self.size_attribute.id,
|
||||
'value_ids': [
|
||||
Command.set([
|
||||
self.size_attribute_m.id,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.authenticate('demo', 'demo')
|
||||
# This should not raise a KeyError
|
||||
result = self.request_get_values(product_template)
|
||||
|
||||
# Verify we got the expected number of attribute lines
|
||||
self.assertEqual(len(result['products'][0]['attribute_lines']), 3)
|
||||
# Verify that each attribute line has its attribute info correctly mapped
|
||||
attribute_names = [
|
||||
line['attribute']['name']
|
||||
for line in result['products'][0]['attribute_lines']
|
||||
]
|
||||
self.assertIn('Size', attribute_names)
|
||||
self.assertIn('Color', attribute_names)
|
||||
# Count occurrences of 'Size' - should be 2 since we have two lines with the same attribute
|
||||
self.assertEqual(attribute_names.count('Size'), 2)
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
# -*- 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)
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleComboConfigurator(HttpCase, SaleCommon):
|
||||
|
||||
def test_sale_combo_configurator(self):
|
||||
if self.env['ir.module.module']._get('sale_management').state != 'installed':
|
||||
self.skipTest("Sale App is not installed, Sale menu is not accessible.")
|
||||
|
||||
no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': "No variant attribute",
|
||||
'create_variant': 'no_variant',
|
||||
'value_ids': [
|
||||
Command.create({'name': "A"}),
|
||||
Command.create({'name': "B", 'is_custom': True, 'default_extra_price': 1}),
|
||||
],
|
||||
})
|
||||
product_a1 = self.env['product.template'].create({
|
||||
'name': "Product A1",
|
||||
'list_price': 100,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': no_variant_attribute.id,
|
||||
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
combo_a = self.env['product.combo'].create({
|
||||
'name': "Combo A",
|
||||
'combo_item_ids': [
|
||||
Command.create({'product_id': product_a1.product_variant_id.id, 'extra_price': 5}),
|
||||
Command.create({'product_id': self._create_product(name="Product A2").id}),
|
||||
],
|
||||
})
|
||||
combo_b = self.env['product.combo'].create({
|
||||
'name': "Combo B",
|
||||
'combo_item_ids': [
|
||||
Command.create({'product_id': self._create_product(name="Product B1").id}),
|
||||
Command.create({'product_id': self._create_product(name="Product B2").id}),
|
||||
],
|
||||
})
|
||||
self._create_product(
|
||||
name="Combo product",
|
||||
list_price=25,
|
||||
type='combo',
|
||||
combo_ids=[
|
||||
Command.link(combo_a.id),
|
||||
Command.link(combo_b.id),
|
||||
],
|
||||
)
|
||||
self.start_tour('/', 'sale_combo_configurator', login='salesman')
|
||||
|
||||
def test_sale_combo_configurator_with_optional_products(self):
|
||||
if self.env['ir.module.module']._get('sale_management').state != 'installed':
|
||||
self.skipTest("Sale App is not installed, Sale menu is not accessible.")
|
||||
|
||||
combo_a = self.env['product.combo'].create({
|
||||
'name': "Combo A",
|
||||
'combo_item_ids': [
|
||||
Command.create({'product_id': self._create_product(name="Product A1").id}),
|
||||
],
|
||||
})
|
||||
combo_b = self.env['product.combo'].create({
|
||||
'name': "Combo B",
|
||||
'combo_item_ids': [
|
||||
Command.create({'product_id': self._create_product(name="Product B1").id}),
|
||||
Command.create({'product_id': self._create_product(name="Product B2").id}),
|
||||
],
|
||||
})
|
||||
optional_product = self.env['product.template'].create({
|
||||
'name': "Optional Product",
|
||||
})
|
||||
combo_product = self.env['product.template'].create({
|
||||
'name': "Combo product",
|
||||
'list_price': 25,
|
||||
'type': 'combo',
|
||||
'combo_ids': [
|
||||
Command.link(combo_a.id),
|
||||
Command.link(combo_b.id),
|
||||
],
|
||||
'optional_product_ids': [Command.link(optional_product.id)],
|
||||
})
|
||||
self.start_tour('/', 'sale_combo_configurator_with_optional_products', login='salesman')
|
||||
|
||||
order = self.env['sale.order'].search([('partner_id.name', '=', 'Test Partner')], limit=1)
|
||||
self.assertTrue(order, "A new Sale order should be created.")
|
||||
self.assertEqual(
|
||||
order.order_line[0].product_template_id,
|
||||
combo_product,
|
||||
"The main combo product should be added",
|
||||
)
|
||||
self.assertEqual(
|
||||
order.order_line[1].product_template_id.name,
|
||||
'Product A1',
|
||||
"Product A1 should be added as a part of this combo",
|
||||
)
|
||||
self.assertEqual(
|
||||
order.order_line[2].product_template_id.name,
|
||||
'Product B2',
|
||||
"Product B2 should be added as a part of this combo",
|
||||
)
|
||||
self.assertEqual(
|
||||
order.order_line[3].product_template_id,
|
||||
optional_product,
|
||||
"Optional product should be added as a part of this combo",
|
||||
)
|
||||
|
||||
def test_sale_combo_configurator_preselect_single_unconfigurable_items(self):
|
||||
self.env['res.users'].search([('login', '=', 'salesman')]).group_ids += self.env.ref("product.group_product_manager")
|
||||
if self.env['ir.module.module']._get('sale_management').state != 'installed':
|
||||
self.skipTest("Sale App is not installed, Sale menu is not accessible.")
|
||||
|
||||
unconfigurable_no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute A",
|
||||
'create_variant': 'no_variant',
|
||||
'value_ids': [Command.create({'name': "A"})],
|
||||
})
|
||||
configurable_no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute B",
|
||||
'create_variant': 'no_variant',
|
||||
'display_type': 'multi',
|
||||
'value_ids': [Command.create({'name': "B"})],
|
||||
})
|
||||
unconfigurable_always_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute C",
|
||||
'create_variant': 'always',
|
||||
'value_ids': [Command.create({'name': "C"})],
|
||||
})
|
||||
configurable_always_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute D",
|
||||
'create_variant': 'always',
|
||||
'value_ids': [Command.create({'name': "D", 'is_custom': True})],
|
||||
})
|
||||
unconfigurable_no_variant_combo = self._create_combo_from_attribute(
|
||||
unconfigurable_no_variant_attribute, "Product A", "Combo A"
|
||||
)
|
||||
configurable_no_variant_combo = self._create_combo_from_attribute(
|
||||
configurable_no_variant_attribute, "Product B", "Combo B"
|
||||
)
|
||||
unconfigurable_always_combo = self._create_combo_from_attribute(
|
||||
unconfigurable_always_attribute, "Product C", "Combo C"
|
||||
)
|
||||
configurable_always_combo = self._create_combo_from_attribute(
|
||||
configurable_always_attribute, "Product D", "Combo D"
|
||||
)
|
||||
combo_with_multiple_unconfigurable_items = self.env['product.combo'].create({
|
||||
'name': "Combo E",
|
||||
'combo_item_ids': [
|
||||
Command.create({'product_id': self._create_product(name="Product E1").id}),
|
||||
Command.create({'product_id': self._create_product(name="Product E2").id}),
|
||||
],
|
||||
})
|
||||
self._create_product(
|
||||
name="Combo product",
|
||||
type='combo',
|
||||
combo_ids=[
|
||||
Command.link(unconfigurable_no_variant_combo.id),
|
||||
Command.link(configurable_no_variant_combo.id),
|
||||
Command.link(unconfigurable_always_combo.id),
|
||||
Command.link(configurable_always_combo.id),
|
||||
Command.link(combo_with_multiple_unconfigurable_items.id),
|
||||
],
|
||||
)
|
||||
self.start_tour(
|
||||
'/', 'sale_combo_configurator_preselect_single_unconfigurable_items', login='salesman'
|
||||
)
|
||||
|
||||
def test_sale_combo_configurator_preconfigure_unconfigurable_ptals(self):
|
||||
if self.env['ir.module.module']._get('sale_management').state != 'installed':
|
||||
self.skipTest("Sale App is not installed, Sale menu is not accessible.")
|
||||
|
||||
unconfigurable_no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute A",
|
||||
'create_variant': 'no_variant',
|
||||
'value_ids': [Command.create({'name': "A"})],
|
||||
})
|
||||
configurable_no_variant_attribute = self.env['product.attribute'].create({
|
||||
'name': "Attribute B",
|
||||
'create_variant': 'no_variant',
|
||||
'display_type': 'multi',
|
||||
'value_ids': [Command.create({'name': "B"})],
|
||||
})
|
||||
product = self.env['product.template'].create({
|
||||
'name': "Test product",
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': unconfigurable_no_variant_attribute.id,
|
||||
'value_ids': [Command.set(unconfigurable_no_variant_attribute.value_ids.ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'attribute_id': configurable_no_variant_attribute.id,
|
||||
'value_ids': [Command.set(configurable_no_variant_attribute.value_ids.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
combo = self.env['product.combo'].create({
|
||||
'name': "Test combo",
|
||||
'combo_item_ids': [Command.create({'product_id': product.product_variant_id.id})],
|
||||
})
|
||||
self._create_product(
|
||||
name="Combo product",
|
||||
type='combo',
|
||||
combo_ids=[Command.link(combo.id)],
|
||||
)
|
||||
self.start_tour(
|
||||
'/', 'sale_combo_configurator_preconfigure_unconfigurable_ptals', login='salesman'
|
||||
)
|
||||
|
||||
def _create_combo_from_attribute(self, attribute, product_name, combo_name):
|
||||
product = self.env['product.template'].create({
|
||||
'name': product_name,
|
||||
'attribute_line_ids': [
|
||||
Command.create({
|
||||
'attribute_id': attribute.id,
|
||||
'value_ids': [Command.set(attribute.value_ids.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
return self.env['product.combo'].create({
|
||||
'name': combo_name,
|
||||
'combo_item_ids': [Command.create({'product_id': product.product_variant_id.id})],
|
||||
})
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.sale.tests.common import TestSaleCommonBase
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
|
||||
class TestSaleFlow(TestSaleCommonBase):
|
||||
class TestSaleFlow(TestSaleCommon):
|
||||
''' Test running at-install to test flows independently to other modules, e.g. 'sale_stock'. '''
|
||||
|
||||
@classmethod
|
||||
|
|
@ -12,7 +11,7 @@ class TestSaleFlow(TestSaleCommonBase):
|
|||
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)],
|
||||
'group_ids': [(6, 0, cls.env.user.group_ids.ids), (4, cls.env.ref('account.group_account_user').id)],
|
||||
})
|
||||
user.partner_id.email = 'saleman@test.com'
|
||||
|
||||
|
|
@ -20,12 +19,6 @@ class TestSaleFlow(TestSaleCommonBase):
|
|||
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,
|
||||
|
|
@ -33,7 +26,6 @@ class TestSaleFlow(TestSaleCommonBase):
|
|||
|
||||
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({
|
||||
|
|
@ -54,15 +46,12 @@ class TestSaleFlow(TestSaleCommonBase):
|
|||
'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, {
|
||||
|
|
@ -70,7 +59,6 @@ class TestSaleFlow(TestSaleCommonBase):
|
|||
'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,
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
# -*- 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()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,46 +0,0 @@
|
|||
# -*- 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)')
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleOrderProductCatalog(HttpCase):
|
||||
|
||||
def test_sale_order_product_catalog_branch_company_tour(self):
|
||||
"""Test adding products to a SO through the catalog view when in a branch company."""
|
||||
|
||||
self.env['product.template'].create({
|
||||
'name': "Restricted Product",
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['res.partner'].create({
|
||||
'name': "Test Partner",
|
||||
})
|
||||
admin = self.env.ref('base.user_admin')
|
||||
branch = self.env['res.company'].with_user(admin).create({
|
||||
'name': "Branch Company",
|
||||
'parent_id': self.env.company.id,
|
||||
})
|
||||
admin.company_id = branch
|
||||
self.env['product.template'].create({
|
||||
'name': "AAA Product",
|
||||
'company_id': admin.company_id.id,
|
||||
})
|
||||
self.start_tour(
|
||||
'/web#action=sale.action_quotations',
|
||||
'sale_catalog',
|
||||
login="admin",
|
||||
)
|
||||
|
||||
def test_add_section_from_product_catalog_on_sale_order_tour(self):
|
||||
self.env['product.template'].create({'name': "Test Product", 'is_favorite': True})
|
||||
self.start_tour(
|
||||
'/web#action=sale.action_quotations',
|
||||
'test_add_section_from_product_catalog_on_sale_order',
|
||||
login='admin',
|
||||
)
|
||||
29
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_order_ui.py
Normal file
29
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_order_ui.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
from odoo.addons.product.tests.common import ProductVariantsCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleOrderUI(HttpCase, ProductVariantsCommon):
|
||||
|
||||
def test_sale_order_keep_uom_on_variant_wizard_quantity_change(self):
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product_template_sofa.product_variant_ids[0].id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})],
|
||||
})
|
||||
|
||||
self.start_tour(
|
||||
f'/odoo/sales/{so.id}',
|
||||
'sale_order_keep_uom_on_variant_wizard_quantity_change',
|
||||
login="admin",
|
||||
)
|
||||
|
||||
sol = so.order_line[0]
|
||||
self.assertEqual(sol.product_uom_id, self.uom_dozen)
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# -*- 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 freezegun import freeze_time
|
||||
|
||||
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.tools import float_compare, float_round, mute_logger
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
|
@ -19,11 +19,18 @@ class TestSalePrices(SaleCommon):
|
|||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls._enable_discounts()
|
||||
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')
|
||||
belgium = cls.env.ref('base.be')
|
||||
cls.env.company.account_fiscal_country_id = belgium
|
||||
for model in ('account.tax', 'account.tax.group'):
|
||||
cls.env.add_to_compute(
|
||||
cls.env[model]._fields['country_id'],
|
||||
cls.env[model].search([('company_id', '=', cls.env.company.id)]),
|
||||
)
|
||||
|
||||
def _create_discount_pricelist_rule(self, **additional_values):
|
||||
return self.env['product.pricelist.item'].create({
|
||||
|
|
@ -40,7 +47,6 @@ class TestSalePrices(SaleCommon):
|
|||
)
|
||||
product_price = self.product.lst_price
|
||||
product_dozen_price = product_price * 12
|
||||
discount = 1 - self.discount/100
|
||||
|
||||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
|
|
@ -58,41 +64,32 @@ class TestSalePrices(SaleCommon):
|
|||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 0.4,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 0.3,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_id': 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.assertTrue(all(not line.discount for line in self.empty_order.order_line - discounted_lines))
|
||||
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))
|
||||
|
||||
discounted_lines[0].product_uom_qty = 3.0
|
||||
self.assertFalse(discounted_lines[0].discount)
|
||||
|
||||
def test_pricelist_dates(self):
|
||||
""" Verify the order date is correctly provided to the pricelist API"""
|
||||
today = fields.Datetime.today()
|
||||
|
|
@ -111,11 +108,9 @@ class TestSalePrices(SaleCommon):
|
|||
'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.assertAlmostEqual(order_line.pricelist_item_id, pricelist_rule)
|
||||
self.assertAlmostEqual(order_line.price_unit, self.product.lst_price)
|
||||
self.assertEqual(order_line.discount, 10)
|
||||
|
||||
# Create an order tomorrow, add line today, rule active today doesn't work
|
||||
self.empty_order.date_order = tomorrow
|
||||
|
|
@ -150,8 +145,8 @@ class TestSalePrices(SaleCommon):
|
|||
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.product.lst_price)
|
||||
self.assertEqual(order_line.discount, 10)
|
||||
|
||||
self.assertEqual(
|
||||
self.empty_order.amount_untaxed,
|
||||
|
|
@ -206,7 +201,7 @@ class TestSalePrices(SaleCommon):
|
|||
self.empty_order.order_line = [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'product_uom_qty': 2.0,
|
||||
}),
|
||||
]
|
||||
|
|
@ -220,8 +215,8 @@ class TestSalePrices(SaleCommon):
|
|||
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,
|
||||
'rate': 2.0,
|
||||
'currency_id': other_currency.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
order_in_other_currency = self.env['sale.order'].create({
|
||||
|
|
@ -230,12 +225,13 @@ class TestSalePrices(SaleCommon):
|
|||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'product_uom_qty': 2.0,
|
||||
}),
|
||||
]
|
||||
})
|
||||
self.assertEqual(order_in_other_currency.amount_total, 480.0)
|
||||
# 20.0 (product price) * 24.0 (2 dozens) * 2.0 (price rate USD -> EUR)
|
||||
self.assertEqual(order_in_other_currency.amount_total, 960.0)
|
||||
|
||||
def test_negative_discounts(self):
|
||||
"""aka surcharges"""
|
||||
|
|
@ -250,7 +246,6 @@ class TestSalePrices(SaleCommon):
|
|||
|
||||
# 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,
|
||||
|
|
@ -264,7 +259,6 @@ class TestSalePrices(SaleCommon):
|
|||
|
||||
base_pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'First pricelist',
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'compute_price': 'percentage',
|
||||
'base': 'list_price',
|
||||
|
|
@ -275,12 +269,11 @@ class TestSalePrices(SaleCommon):
|
|||
})
|
||||
|
||||
self.pricelist.write({
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'compute_price': 'formula',
|
||||
'compute_price': 'percentage',
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': base_pricelist.id,
|
||||
'price_discount': 10,
|
||||
'percent_price': 10,
|
||||
'applied_on': '3_global',
|
||||
'name': 'Second discount',
|
||||
})],
|
||||
|
|
@ -297,7 +290,9 @@ class TestSalePrices(SaleCommon):
|
|||
|
||||
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")
|
||||
self.assertEqual(
|
||||
order_line.discount, 19,
|
||||
"Discount not computed correctly based on both pricelists")
|
||||
|
||||
def test_pricelist_with_another_currency(self):
|
||||
""" Test prices are correctly applied with a pricelist with another currency"""
|
||||
|
|
@ -316,17 +311,14 @@ class TestSalePrices(SaleCommon):
|
|||
).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,
|
||||
'relative_factor': 10,
|
||||
'relative_uom_id': self.uom_unit.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',
|
||||
|
|
@ -347,13 +339,76 @@ class TestSalePrices(SaleCommon):
|
|||
})
|
||||
|
||||
# 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")
|
||||
self.assertEqual(order_line.discount, 10, "First pricelist rule not applied")
|
||||
order_line.product_uom_id = new_uom
|
||||
self.assertEqual(order_line.price_total, 1800, "First pricelist rule not applied")
|
||||
|
||||
def test_pricelist_price_recompute_on_quantity_change(self):
|
||||
"""
|
||||
Test price updates correctly when quantity changes with
|
||||
pricelist based on another pricelist.
|
||||
"""
|
||||
self._enable_pricelists()
|
||||
|
||||
pricelist_a = self.env['product.pricelist'].create({
|
||||
'name': "Pricelist A",
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
'applied_on': '3_global',
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 0.75,
|
||||
'min_quantity': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'applied_on': '3_global',
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 0.50,
|
||||
'min_quantity': 1000,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
pricelist_b = self.env['product.pricelist'].create({
|
||||
'name': "Pricelist B",
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
'applied_on': '3_global',
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': -10,
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': pricelist_a.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
with Form(self.env['sale.order']) as order_form:
|
||||
order_form.partner_id = self.partner
|
||||
order_form.pricelist_id = pricelist_b
|
||||
with order_form.order_line.new() as line_form:
|
||||
line_form.product_id = self.product
|
||||
self.assertAlmostEqual(line_form.price_unit, 0.825)
|
||||
line_form.product_uom_qty = 1000
|
||||
self.assertAlmostEqual(line_form.price_unit, 0.55)
|
||||
|
||||
def test_compute_price_unit_no_currency(self):
|
||||
new_order = self.env['sale.order'].new({
|
||||
'currency_id': False,
|
||||
'pricelist_id': False,
|
||||
'order_line': [Command.create({'product_id': self.product.id})],
|
||||
})
|
||||
new_line = new_order.order_line
|
||||
self.assertEqual(new_line.price_unit, self.product.list_price)
|
||||
|
||||
new_line.price_unit = new_price = self.product.list_price + 0.5
|
||||
new_line.product_uom_qty += 1.0
|
||||
self.assertEqual(new_line.price_unit, new_price, "Manual unit price shouldn't change")
|
||||
|
||||
new_order._recompute_prices()
|
||||
self.assertEqual(new_line.price_unit, self.product.list_price)
|
||||
|
||||
def test_multi_currency_discount(self):
|
||||
"""Verify the currency used for pricelist price & discount computation."""
|
||||
product_1 = self.consumable_product
|
||||
product_1 = self.product
|
||||
product_2 = self.service_product
|
||||
|
||||
# Make sure the company is in USD
|
||||
|
|
@ -371,7 +426,12 @@ class TestSalePrices(SaleCommon):
|
|||
'company_ids': [Command.set([other_company.id])],
|
||||
'name': 'E.T',
|
||||
'login': 'hohoho',
|
||||
'group_ids': (
|
||||
self.env.ref('sales_team.group_sale_salesman') |
|
||||
self.env.ref('product.group_product_manager')
|
||||
),
|
||||
})
|
||||
user_in_other_company.group_ids += self.env.ref("product.group_product_manager")
|
||||
with mute_logger('odoo.models.unlink'):
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
|
|
@ -401,7 +461,7 @@ class TestSalePrices(SaleCommon):
|
|||
|
||||
pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'Test multi-currency',
|
||||
'discount_policy': 'without_discount',
|
||||
'company_id': False,
|
||||
'currency_id': other_curr.id,
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
|
|
@ -428,7 +488,7 @@ class TestSalePrices(SaleCommon):
|
|||
# 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,
|
||||
'partner_id': user_in_other_company.partner_id.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
|
|
@ -455,7 +515,7 @@ class TestSalePrices(SaleCommon):
|
|||
# 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,
|
||||
'partner_id': user_in_other_company.partner_id.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'order_line': [
|
||||
# Verify discount is considered in create hack
|
||||
|
|
@ -485,6 +545,7 @@ class TestSalePrices(SaleCommon):
|
|||
"""
|
||||
sale_order = self.sale_order
|
||||
so_amount = sale_order.amount_total
|
||||
start_so_amount = so_amount
|
||||
sale_order._recompute_prices()
|
||||
self.assertEqual(
|
||||
sale_order.amount_total, so_amount,
|
||||
|
|
@ -492,42 +553,77 @@ class TestSalePrices(SaleCommon):
|
|||
|
||||
pricelist = sale_order.pricelist_id
|
||||
pricelist.item_ids = [
|
||||
fields.Command.create({
|
||||
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"
|
||||
pricelist.item_ids = [
|
||||
Command.create({
|
||||
'price_discount': 5,
|
||||
'compute_price': 'formula',
|
||||
})
|
||||
]
|
||||
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)
|
||||
|
||||
# Test taking off the pricelist
|
||||
sale_order.pricelist_id = False
|
||||
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, start_so_amount,
|
||||
"The SO amount without pricelist should be the same than with an empty pricelist"
|
||||
)
|
||||
|
||||
def test_manual_price_prevents_recompute(self):
|
||||
sale_order_line = self.sale_order.order_line[0]
|
||||
# Ensure initial price is set correctly
|
||||
self.assertEqual(sale_order_line.price_unit, 20.0)
|
||||
|
||||
# Update the price manually and then change the quantity
|
||||
with Form(sale_order_line) as line:
|
||||
line.price_unit = 100.0
|
||||
line.product_uom_qty = 10
|
||||
|
||||
self.assertEqual(
|
||||
sale_order_line.price_unit, 100.0,
|
||||
"Price should remain 100.0 after changing the quantity"
|
||||
)
|
||||
|
||||
zero_price_product = self._create_product(list_price=0.0)
|
||||
self.assertEqual(zero_price_product.list_price, 0.0)
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'product_id': zero_price_product.id,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
self.assertEqual(so_line.price_unit, 0.0)
|
||||
self.assertEqual(so_line.technical_price_unit, 0.0)
|
||||
|
||||
with Form(so_line) as so_line:
|
||||
so_line.price_unit = 10.0
|
||||
so_line.product_uom_qty = 2.0
|
||||
so_line.save()
|
||||
|
||||
self.assertEqual(so_line.price_unit, 10.0)
|
||||
|
||||
# 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)
|
||||
|
|
@ -535,11 +631,19 @@ class TestSalePrices(SaleCommon):
|
|||
'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
|
||||
})]
|
||||
})
|
||||
tax_a, tax_b = self.env['account.tax'].create([{
|
||||
'name': 'Test tax A',
|
||||
'type_tax_use': 'sale',
|
||||
'price_include_override': 'tax_included',
|
||||
'amount': 15.0,
|
||||
}, {
|
||||
'name': 'Test tax B',
|
||||
'type_tax_use': 'sale',
|
||||
'amount': 6.0,
|
||||
'fiscal_position_ids': fiscal_pos,
|
||||
}])
|
||||
tax_b.original_tax_ids = tax_a
|
||||
|
||||
# setting up partner:
|
||||
self.partner.country_id = country_belgium
|
||||
|
|
@ -550,7 +654,6 @@ class TestSalePrices(SaleCommon):
|
|||
})
|
||||
|
||||
self.pricelist.write({
|
||||
'discount_policy': 'without_discount',
|
||||
'item_ids': [Command.create({
|
||||
'applied_on': '3_global',
|
||||
'compute_price': 'percentage',
|
||||
|
|
@ -584,7 +687,7 @@ class TestSalePrices(SaleCommon):
|
|||
"Wrong subtotal price computed for specified product & pricelist"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.empty_order.order_line.tax_id.id, tax_b.id,
|
||||
self.empty_order.order_line.tax_ids.id, tax_b.id,
|
||||
"Wrong tax applied for specified product & pricelist"
|
||||
)
|
||||
|
||||
|
|
@ -598,6 +701,25 @@ class TestSalePrices(SaleCommon):
|
|||
pricelist = self.pricelist
|
||||
partner = self.partner
|
||||
|
||||
(
|
||||
fpos_incl_incl,
|
||||
fpos_excl_incl,
|
||||
fpos_incl_excl,
|
||||
fpos_excl_excl,
|
||||
) = self.env['account.fiscal.position'].create([{
|
||||
'name': "incl -> incl",
|
||||
'sequence': 1,
|
||||
}, {
|
||||
'name': "excl -> incl",
|
||||
'sequence': 2,
|
||||
}, {
|
||||
'name': "incl -> excl",
|
||||
'sequence': 3,
|
||||
}, {
|
||||
'name': "excl -> excl",
|
||||
'sequence': 4,
|
||||
}])
|
||||
|
||||
(
|
||||
tax_fixed_incl,
|
||||
tax_fixed_excl,
|
||||
|
|
@ -607,36 +729,45 @@ class TestSalePrices(SaleCommon):
|
|||
tax_exclude_dst,
|
||||
) = self.env['account.tax'].create([{
|
||||
'name': "fixed include",
|
||||
'amount': '10.00',
|
||||
'amount': 10.00,
|
||||
'amount_type': 'fixed',
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
}, {
|
||||
'name': "fixed exclude",
|
||||
'amount': '10.00',
|
||||
'amount': 10.00,
|
||||
'amount_type': 'fixed',
|
||||
'price_include': False,
|
||||
'price_include_override': 'tax_excluded',
|
||||
}, {
|
||||
'name': "Include 21%",
|
||||
'amount': 21.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
}, {
|
||||
'name': "Include 6%",
|
||||
'amount': 6.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
}, {
|
||||
'name': "Exclude 15%",
|
||||
'amount': 15.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': False,
|
||||
'price_include_override': 'tax_excluded',
|
||||
}, {
|
||||
'name': "Exclude 21%",
|
||||
'amount': 21.00,
|
||||
'amount_type': 'percent',
|
||||
'price_include': False,
|
||||
'price_include_override': 'tax_excluded',
|
||||
}])
|
||||
|
||||
tax_include_dst.write({
|
||||
'fiscal_position_ids': fpos_incl_incl | fpos_excl_incl,
|
||||
'original_tax_ids': tax_include_src | tax_exclude_src,
|
||||
})
|
||||
tax_exclude_dst.write({
|
||||
'fiscal_position_ids': fpos_incl_excl | fpos_excl_excl,
|
||||
'original_tax_ids': tax_include_src | tax_exclude_src,
|
||||
})
|
||||
|
||||
(
|
||||
product_tmpl_a,
|
||||
product_tmpl_b,
|
||||
|
|
@ -660,41 +791,6 @@ class TestSalePrices(SaleCommon):
|
|||
'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
|
||||
|
||||
|
|
@ -775,31 +871,29 @@ class TestSalePrices(SaleCommon):
|
|||
def test_so_tax_mapping(self):
|
||||
order = self.empty_order
|
||||
|
||||
fpos = self.env['account.fiscal.position'].create({
|
||||
'name': 'Test Fiscal Position',
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
tax_include, tax_exclude = self.env['account.tax'].create([{
|
||||
'name': 'Include Tax',
|
||||
'amount': '21.00',
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'type_tax_use': 'sale',
|
||||
}, {
|
||||
'name': 'Exclude Tax',
|
||||
'amount': '0.00',
|
||||
'type_tax_use': 'sale',
|
||||
'fiscal_position_ids': fpos,
|
||||
}])
|
||||
tax_exclude.original_tax_ids = tax_include
|
||||
|
||||
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({
|
||||
|
|
@ -812,6 +906,46 @@ class TestSalePrices(SaleCommon):
|
|||
100, order.order_line[0].price_unit,
|
||||
"The included tax must be subtracted to the price")
|
||||
|
||||
def test_so_tax_mapping_multicompany(self):
|
||||
fpos = self.env['account.fiscal.position'].create({'name': "B2B"})
|
||||
tax_group = self.env['account.tax.group'].create({'name': "10%"})
|
||||
tax_include = self.env['account.tax'].create({
|
||||
'name': "10% Tax Inc.",
|
||||
'type_tax_use': 'sale',
|
||||
'amount': 10.0,
|
||||
'price_include_override': 'tax_included',
|
||||
'tax_group_id': tax_group.id,
|
||||
'fiscal_position_ids': fpos.ids,
|
||||
})
|
||||
tax_exclude = tax_include.copy({
|
||||
'name': "10% Tax Exc.",
|
||||
'amount': 0.0,
|
||||
'price_include_override': 'tax_excluded',
|
||||
'original_tax_ids': tax_include.ids,
|
||||
})
|
||||
self.product.write({
|
||||
'list_price': 110.0,
|
||||
'taxes_id': tax_include.ids,
|
||||
})
|
||||
branch_company = self.env['res.company'].create({
|
||||
'name': "Branch Co.",
|
||||
'parent_id': self.env.company.id,
|
||||
'account_fiscal_country_id': self.env.company.account_fiscal_country_id.id,
|
||||
})
|
||||
order = self.empty_order.with_company(branch_company)
|
||||
order.sudo().write({
|
||||
'company_id': branch_company.id,
|
||||
'fiscal_position_id': fpos.id,
|
||||
'user_id': False,
|
||||
'team_id': False,
|
||||
'order_line': [Command.create({'product_id': self.product.id})],
|
||||
})
|
||||
self.assertEqual(order.order_line.tax_ids, tax_exclude, "Line tax should be mapped")
|
||||
self.assertAlmostEqual(
|
||||
order.order_line.price_unit, 100.0,
|
||||
msg="Tax should not be included in unit 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([{
|
||||
|
|
@ -819,14 +953,14 @@ class TestSalePrices(SaleCommon):
|
|||
'type_tax_use': 'sale',
|
||||
'amount_type': 'fixed',
|
||||
'amount': 0.05,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
}, {
|
||||
'name': 'Recupel 0.25',
|
||||
'type_tax_use': 'sale',
|
||||
'amount_type': 'fixed',
|
||||
'amount': 0.25,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
'include_base_amount': True,
|
||||
}])
|
||||
order = self.empty_order
|
||||
|
|
@ -834,7 +968,7 @@ class TestSalePrices(SaleCommon):
|
|||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 0.0,
|
||||
'tax_id': [
|
||||
'tax_ids': [
|
||||
Command.set(taxes.ids),
|
||||
],
|
||||
})]
|
||||
|
|
@ -855,18 +989,18 @@ class TestSalePrices(SaleCommon):
|
|||
tax_include, tax_exclude = self.env['account.tax'].create([{
|
||||
'name': 'Tax with price include',
|
||||
'amount': 10,
|
||||
'price_include': True
|
||||
'price_include_override': 'tax_included',
|
||||
}, {
|
||||
'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)]})
|
||||
self.sale_order.order_line[0].write({'tax_ids': [Command.link(tax_include.id)]})
|
||||
self.sale_order.order_line[1].write({'tax_ids': [Command.link(tax_exclude.id)]})
|
||||
|
||||
for line in self.sale_order.order_line:
|
||||
if line.tax_id.price_include:
|
||||
if line.tax_ids.price_include:
|
||||
price = line.price_unit * line.product_uom_qty - line.price_tax
|
||||
else:
|
||||
price = line.price_unit * line.product_uom_qty
|
||||
|
|
@ -902,11 +1036,11 @@ class TestSalePrices(SaleCommon):
|
|||
# Same with an included-in-price tax
|
||||
order = order.copy()
|
||||
line = order.order_line
|
||||
line.tax_id = [Command.create({
|
||||
line.tax_ids = [Command.create({
|
||||
'name': 'Super Tax',
|
||||
'amount_type': 'percent',
|
||||
'amount': 15.0,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
})]
|
||||
order.action_confirm()
|
||||
self.assertEqual(line.untaxed_amount_to_invoice, 0)
|
||||
|
|
@ -938,6 +1072,7 @@ class TestSalePrices(SaleCommon):
|
|||
order_line.write({
|
||||
'product_uom_qty': 3.0,
|
||||
'price_unit': 100.0,
|
||||
'discount': 1.0,
|
||||
})
|
||||
order.invalidate_recordset(['amount_undiscounted'])
|
||||
|
||||
|
|
@ -952,11 +1087,11 @@ class TestSalePrices(SaleCommon):
|
|||
# Same with an included-in-price tax
|
||||
order = order.copy()
|
||||
line = order.order_line
|
||||
line.tax_id = [Command.create({
|
||||
line.tax_ids = [Command.create({
|
||||
'name': 'Super Tax',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10.0,
|
||||
'price_include': True,
|
||||
'price_include_override': 'tax_included',
|
||||
})]
|
||||
line.discount = 50.0
|
||||
order.action_confirm()
|
||||
|
|
@ -979,7 +1114,134 @@ class TestSalePrices(SaleCommon):
|
|||
})]
|
||||
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)
|
||||
quantity_precision = self.env['decimal.precision'].precision_get('Product Unit')
|
||||
self.assertEqual(
|
||||
line.product_uom_qty, float_round(product_uom_qty, precision_digits=quantity_precision))
|
||||
expected_price_subtotal = line.currency_id.round(
|
||||
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'))
|
||||
self.assertEqual(order.amount_total, order.tax_totals.get('total_amount_currency'))
|
||||
|
||||
def test_show_discount(self):
|
||||
"""
|
||||
Test that discount is shown only when compute_price is percentage
|
||||
If compute_price is formula, discount should be included in price.
|
||||
"""
|
||||
test_product_discount = self.env['product.product'].create({
|
||||
'name': 'Test Product',
|
||||
'list_price': 100.0,
|
||||
'taxes_id': None,
|
||||
})
|
||||
test_product_incl_discount = self.env['product.product'].create({
|
||||
'name': 'Test Product',
|
||||
'list_price': 100.0,
|
||||
'taxes_id': None,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': test_product_discount.id,
|
||||
'product_uom_qty': 1.0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': test_product_incl_discount.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
self.assertEqual(200, sale_order.amount_total)
|
||||
base_discount_pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'Base Discount Pricelist',
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
'name': 'Discount',
|
||||
'applied_on': '1_product',
|
||||
'product_tmpl_id': test_product_discount.product_tmpl_id.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 10,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Formula',
|
||||
'applied_on': '1_product',
|
||||
'product_tmpl_id': test_product_incl_discount.product_tmpl_id.id,
|
||||
'compute_price': 'formula',
|
||||
'price_discount': 10,
|
||||
}),
|
||||
]})
|
||||
|
||||
sale_order.pricelist_id = base_discount_pricelist
|
||||
sale_order._recompute_prices()
|
||||
show_discount_line = sale_order.order_line[0]
|
||||
included_discount_line = sale_order.order_line[1]
|
||||
|
||||
self.assertEqual(show_discount_line.price_unit, 100)
|
||||
self.assertEqual(show_discount_line.price_subtotal, show_discount_line.price_unit * 0.9)
|
||||
self.assertEqual(show_discount_line.discount, 10)
|
||||
self.assertEqual(included_discount_line.price_unit, included_discount_line.price_subtotal)
|
||||
self.assertEqual(included_discount_line.discount, 0)
|
||||
|
||||
# Test with discount based on other pricelist
|
||||
discount_pricelist = self.env['product.pricelist'].create({
|
||||
'name': 'Discount Pricelist',
|
||||
'item_ids': [
|
||||
Command.create({
|
||||
'name': 'Discount based on pricelist',
|
||||
'applied_on': '1_product',
|
||||
'product_tmpl_id': test_product_discount.product_tmpl_id.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 10,
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': base_discount_pricelist.id,
|
||||
}),
|
||||
]})
|
||||
sale_order.pricelist_id = discount_pricelist
|
||||
sale_order._recompute_prices()
|
||||
|
||||
self.assertEqual(show_discount_line.price_unit, 100)
|
||||
self.assertEqual(show_discount_line.price_subtotal, show_discount_line.price_unit * 0.81)
|
||||
self.assertEqual(show_discount_line.discount, 19)
|
||||
|
||||
def test_combo_product_discount(self):
|
||||
"""Ensure that pricelist discounts for combo products get applied to combo items"""
|
||||
order = self.empty_order
|
||||
|
||||
product_a = self._create_product(name="Beefy burger")
|
||||
product_b = self._create_product(name="Belgian fries")
|
||||
combos = self.env['product.combo'].create([{
|
||||
'name': "Burger",
|
||||
'combo_item_ids': [Command.create({'product_id': product_a.id})],
|
||||
}, {
|
||||
'name': "Side",
|
||||
'combo_item_ids': [Command.create({'product_id': product_b.id})],
|
||||
}])
|
||||
product_combo = self._create_product(
|
||||
name="Meal Menu",
|
||||
list_price=10.0,
|
||||
type='combo',
|
||||
combo_ids=[Command.set(combos.ids)],
|
||||
)
|
||||
|
||||
self._create_discount_pricelist_rule(product_tmpl_id=product_combo.product_tmpl_id.id)
|
||||
combo_line = self.env['sale.order.line'].create({
|
||||
'order_id': order.id,
|
||||
'product_id': product_combo.id,
|
||||
})
|
||||
item_lines = self.env['sale.order.line'].create([{
|
||||
'order_id': order.id,
|
||||
'product_id': product.id,
|
||||
'combo_item_id': combo.combo_item_ids.id,
|
||||
'linked_line_id': combo_line.id,
|
||||
} for product, combo in zip(product_a + product_b, combos)])
|
||||
|
||||
self.assertEqual(
|
||||
item_lines.mapped('discount'),
|
||||
[self.discount, self.discount],
|
||||
"Discount should apply to combo item lines",
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
order.amount_untaxed,
|
||||
order.amount_undiscounted * (100 - self.discount) / 100,
|
||||
msg="Pricelist discount should be applied to quotation",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,96 +1,14 @@
|
|||
# -*- 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,
|
||||
})
|
||||
from odoo.addons.product.tests.test_product_attribute_value_config import (
|
||||
TestProductAttributeValueCommon,
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
class TestSaleProductAttributeValueConfig(TestProductAttributeValueCommon):
|
||||
|
||||
def test_01_is_combination_possible_archived(self):
|
||||
"""The goal is to test the possibility of archived combinations.
|
||||
|
|
@ -202,320 +120,3 @@ class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
|
|||
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()
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo.tests import tagged, users
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleProductTemplate(SaleCommon):
|
||||
|
||||
@users('salesman')
|
||||
def test_sale_get_configurator_display_price(self):
|
||||
configurator_price = self.env['product.template']._get_configurator_display_price(
|
||||
product_or_template=self._create_product(list_price=40),
|
||||
quantity=3,
|
||||
date=datetime(2000, 1, 1),
|
||||
currency=self.currency,
|
||||
pricelist=self.pricelist,
|
||||
)
|
||||
|
||||
self.assertEqual(configurator_price[0], 40)
|
||||
|
||||
@users('salesman')
|
||||
def test_sale_get_additional_configurator_data(self):
|
||||
configurator_data = self.env['product.template']._get_additional_configurator_data(
|
||||
product_or_template=self.product,
|
||||
date=datetime(2000, 1, 1),
|
||||
currency=self.currency,
|
||||
pricelist=self.pricelist,
|
||||
)
|
||||
|
||||
self.assertEqual(configurator_data, {})
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
|
|
@ -11,35 +10,34 @@ from odoo.addons.sale.tests.common import TestSaleCommon
|
|||
class TestSaleRefund(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create the SO with four order lines
|
||||
cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
cls.sale_order = cls.env['sale.order'].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,
|
||||
'tax_ids': False,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.company_data['product_service_delivery'].id,
|
||||
'product_uom_qty': 4,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.company_data['product_service_order'].id,
|
||||
'product_uom_qty': 3,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.company_data['product_delivery_no'].id,
|
||||
'product_uom_qty': 2,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
|
@ -92,11 +90,10 @@ class TestSaleRefund(TestSaleCommon):
|
|||
|
||||
# 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()
|
||||
credit_note_wizard.refund_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
|
||||
|
|
@ -153,67 +150,6 @@ class TestSaleRefund(TestSaleCommon):
|
|||
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
|
||||
|
|
@ -247,11 +183,10 @@ class TestSaleRefund(TestSaleCommon):
|
|||
|
||||
# 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'])
|
||||
invoice_refund = self.env['account.move'].browse(credit_note_wizard.modify_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')
|
||||
|
|
@ -320,13 +255,12 @@ class TestSaleRefund(TestSaleCommon):
|
|||
'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,
|
||||
'tax_ids': False,
|
||||
})
|
||||
|
||||
self.assertRecordValues(sol_product, [{
|
||||
|
|
@ -352,7 +286,6 @@ class TestSaleRefund(TestSaleCommon):
|
|||
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
|
||||
|
|
@ -371,9 +304,7 @@ class TestSaleRefund(TestSaleCommon):
|
|||
'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 = self.env['sale.advance.payment.inv'].with_context(so_context).create({})
|
||||
payment.create_invoices()
|
||||
|
||||
so_invoice = max(sale_order_refund.invoice_ids)
|
||||
|
|
@ -384,11 +315,10 @@ class TestSaleRefund(TestSaleCommon):
|
|||
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()
|
||||
credit_note_wizard.refund_moves()
|
||||
invoice_refund = sale_order_refund.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1]
|
||||
invoice_refund.action_post()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# -*- 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.tools.float_utils import float_compare
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
|
@ -40,9 +40,9 @@ class TestSaleReportCurrencyRate(SaleCommon):
|
|||
|
||||
# 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},
|
||||
{'name': 'Pricelist (USD)', 'currency_id': usd.id, 'company_id': False},
|
||||
{'name': 'Pricelist (EUR)', 'currency_id': eur.id, 'company_id': False},
|
||||
{'name': 'Pricelist (ARS)', 'currency_id': ars.id, 'company_id': False},
|
||||
])
|
||||
self.env['res.currency.rate'].create([
|
||||
{'name': past_day, 'rate': 555, 'currency_id': ars.id, 'company_id': self.eur_cmp.id},
|
||||
|
|
@ -101,11 +101,8 @@ class TestSaleReportCurrencyRate(SaleCommon):
|
|||
# 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
|
||||
expected_amount_total = pricelist.currency_id.round(qty * price_for_so_company * expected_so_currency_rate)
|
||||
self.assertAlmostEqual(order.currency_rate, expected_so_currency_rate)
|
||||
self.assertAlmostEqual(order.amount_total, expected_amount_total)
|
||||
|
||||
|
|
@ -121,7 +118,33 @@ class TestSaleReportCurrencyRate(SaleCommon):
|
|||
# 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)])
|
||||
).search([('order_reference', 'in', [f'sale.order,{so_id}' for so_id in sale_orders.ids])])
|
||||
|
||||
price_total = sum(report_lines.mapped('price_total'))
|
||||
self.assertEqual(price_total, expected_reported_amount)
|
||||
self.assertAlmostEqual(price_total, expected_reported_amount)
|
||||
|
||||
def test_sale_report_with_downpayment(self):
|
||||
"""Checks that downpayment lines are used in the calculation of amounts invoiced and to invoice"""
|
||||
order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product.id,
|
||||
})]
|
||||
})
|
||||
order.action_confirm()
|
||||
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({
|
||||
'advance_payment_method': 'fixed',
|
||||
'fixed_amount': 200
|
||||
})
|
||||
downpayment.create_invoices()
|
||||
order.invoice_ids.action_post()
|
||||
order.order_line.flush_recordset()
|
||||
|
||||
amount_line = self.env['sale.report'].formatted_read_group(
|
||||
[('order_reference', '=', f'sale.order,{order.id}')],
|
||||
aggregates=['untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum'],
|
||||
)[0]
|
||||
|
||||
self.assertEqual(float_compare(amount_line['untaxed_amount_invoiced:sum'], 200, precision_rounding=order.currency_id.rounding), 0)
|
||||
self.assertEqual(float_compare(amount_line['untaxed_amount_to_invoice:sum'], self.product.lst_price - 200, precision_rounding=order.currency_id.rounding), 0)
|
||||
|
|
|
|||
142
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_sections.py
Normal file
142
odoo-bringout-oca-ocb-sale/sale/tests/test_sale_sections.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import SaleCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleSections(SaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.tax_1 = cls.env['account.tax'].create({
|
||||
'name': 'Tax 1',
|
||||
'amount': 10,
|
||||
})
|
||||
cls.tax_2 = cls.env['account.tax'].create({
|
||||
'name': 'Tax 2',
|
||||
'amount': 20,
|
||||
})
|
||||
cls.sections_sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'name': 'r1',
|
||||
'product_id': cls.product.id,
|
||||
'price_unit': 10,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1',
|
||||
'display_type': 'line_section',
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-r1',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': cls.tax_2.ids,
|
||||
'price_unit': 200,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub1',
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub1-r1',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': (cls.tax_1 + cls.tax_2).ids,
|
||||
'price_unit': 300,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub1-r2',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': (cls.tax_1 + cls.tax_2).ids,
|
||||
'price_unit': 300,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub1-r3',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': cls.tax_1.ids,
|
||||
'price_unit': 100,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub2',
|
||||
'display_type': 'line_subsection',
|
||||
'collapse_composition': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub2-r1',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': cls.tax_2.ids,
|
||||
'price_unit': 200,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Sec1-Sub2-r2',
|
||||
'product_id': cls.product.id,
|
||||
'tax_ids': cls.tax_1.ids,
|
||||
'price_unit': 100,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
def test_sale_order_line_parent_id(self):
|
||||
"""Verify correct assignment of `parent_id`:
|
||||
|
||||
- Lines with no preceding section/subsection → no parent.
|
||||
- Section's children (lines + subsections) → parent is the section.
|
||||
- Subsection's children → parent is the subsection.
|
||||
"""
|
||||
self.assertFalse(self.sections_sale_order.order_line[0].parent_id)
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[2].parent_id,
|
||||
self.sections_sale_order.order_line[1],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[3].parent_id,
|
||||
self.sections_sale_order.order_line[1],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[4].parent_id,
|
||||
self.sections_sale_order.order_line[3],
|
||||
)
|
||||
|
||||
def test_sale_order_report_line_visibility_and_grouping(self):
|
||||
"""Check report utils for sections.
|
||||
|
||||
- `_get_order_lines_to_report` must exclude children of collapsed sections/subsections,
|
||||
but keep regular lines and headers in order.
|
||||
- `_get_grouped_section_summary` must correctly aggregate totals by tax.
|
||||
"""
|
||||
lines_to_report = self.sections_sale_order._get_order_lines_to_report()
|
||||
self.assertEqual(len(lines_to_report), 5)
|
||||
self.assertEqual(
|
||||
lines_to_report.mapped('name'),
|
||||
['r1', 'Sec1', 'Sec1-r1', 'Sec1-Sub1', 'Sec1-Sub2'],
|
||||
"Lines of hidden subsection shouldn't be visible in report",
|
||||
)
|
||||
|
||||
subsection_summary_lines = lines_to_report[3]._get_grouped_section_summary()
|
||||
self.assertEqual(len(subsection_summary_lines), 2)
|
||||
|
||||
self.assertEqual(subsection_summary_lines[0]['price_subtotal'], 600.00)
|
||||
self.assertEqual(subsection_summary_lines[1]['price_subtotal'], 100.00)
|
||||
|
||||
def test_sale_order_sections_totals(self):
|
||||
"""Ensure section totals are computed correctly.
|
||||
|
||||
A `line_section` should aggregate the subtotals of all following product
|
||||
order lines that belong to it, including those under nested subsections.
|
||||
Aggregation must stop when another section or subsection is encountered.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[1]._get_section_totals('price_subtotal'),
|
||||
sum(self.sections_sale_order.order_line[1:].mapped('price_subtotal')),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[3]._get_section_totals('price_subtotal'),
|
||||
sum(self.sections_sale_order.order_line[4:7].mapped('price_subtotal')),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.sections_sale_order.order_line[7]._get_section_totals('price_subtotal'),
|
||||
sum(self.sections_sale_order.order_line[8:].mapped('price_subtotal')),
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# -*- 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,
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
1118
odoo-bringout-oca-ocb-sale/sale/tests/test_taxes_downpayment.py
Normal file
1118
odoo-bringout-oca-ocb-sale/sale/tests/test_taxes_downpayment.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,202 @@
|
|||
from odoo import Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account.tests.test_taxes_global_discount import TestTaxesGlobalDiscount
|
||||
from odoo.addons.sale.tests.common import TestTaxCommonSale
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxesGlobalDiscountSale(TestTaxCommonSale, TestTaxesGlobalDiscount):
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def assert_sale_order_global_discount(
|
||||
self,
|
||||
sale_order,
|
||||
amount_type,
|
||||
amount,
|
||||
expected_values,
|
||||
soft_checking=False,
|
||||
):
|
||||
""" Assert the expected values for the tax totals summary of the sale order
|
||||
passed as parameter after the applicability of a global discount.
|
||||
|
||||
:param sale_order: The SO as a sale.order record.
|
||||
:param amount_type: The type of the global discount: 'percent' or 'fixed'.
|
||||
:param amount: The amount to consider.
|
||||
For 'percent', it should be a percentage [0-100].
|
||||
For 'fixed', any amount.
|
||||
:param expected_values: The expected values for the tax_total_summary.
|
||||
:param soft_checking: Limit the asserted values to the ones in 'expected_results'
|
||||
and don't go deeper inside the tax_total_summary.
|
||||
It allows to assert only the totals without asserting all the
|
||||
tax details.
|
||||
"""
|
||||
if amount_type == 'percent':
|
||||
discount_type = 'so_discount'
|
||||
discount_percentage = amount / 100.0
|
||||
discount_amount = None
|
||||
else: # amount_type == 'fixed'
|
||||
discount_type = 'amount'
|
||||
discount_percentage = None
|
||||
discount_amount = amount
|
||||
discount_wizard = (
|
||||
self.env['sale.order.discount']
|
||||
.with_context({'active_model': sale_order._name, 'active_id': sale_order.id})
|
||||
.create({
|
||||
'discount_type': discount_type,
|
||||
'discount_percentage': discount_percentage,
|
||||
'discount_amount': discount_amount,
|
||||
})
|
||||
)
|
||||
discount_wizard.action_apply_discount()
|
||||
self._assert_tax_totals_summary(
|
||||
sale_order.tax_totals,
|
||||
expected_values,
|
||||
soft_checking=soft_checking,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# GENERIC TAXES TEST SUITE
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_taxes_l10n_in_sale_orders(self):
|
||||
for test_mode, document, soft_checking, amount_type, amount, expected_values in self._test_taxes_l10n_in():
|
||||
with self.subTest(test_code=test_mode, amount=amount):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
sale_order.action_confirm()
|
||||
self.assert_sale_order_global_discount(sale_order, amount_type, amount, expected_values, soft_checking=soft_checking)
|
||||
|
||||
def test_taxes_l10n_br_sale_orders(self):
|
||||
for test_mode, document, soft_checking, amount_type, amount, expected_values in self._test_taxes_l10n_br():
|
||||
with self.subTest(test_code=test_mode, amount=amount):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
sale_order.action_confirm()
|
||||
self.assert_sale_order_global_discount(sale_order, amount_type, amount, expected_values, soft_checking=soft_checking)
|
||||
|
||||
def test_taxes_l10n_be_sale_orders(self):
|
||||
for test_mode, document, soft_checking, amount_type, amount, expected_values in self._test_taxes_l10n_be():
|
||||
with self.subTest(test_code=test_mode, amount=amount):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
sale_order.action_confirm()
|
||||
self.assert_sale_order_global_discount(sale_order, amount_type, amount, expected_values, soft_checking=soft_checking)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# SPECIFIC TESTS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_global_discount_with_sol_discount(self):
|
||||
product = self.company_data['product_order_cost']
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
# Standalone line with a discount already:
|
||||
Command.create({
|
||||
'name': 'line_1',
|
||||
'product_id': product.id,
|
||||
'price_unit': 1000.0,
|
||||
'discount': 50.0,
|
||||
}),
|
||||
# Standalone line without any discount:
|
||||
Command.create({
|
||||
'name': 'line_2',
|
||||
'product_id': product.id,
|
||||
'price_unit': 2000.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
so.action_confirm()
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 2500.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 2500.0,
|
||||
}])
|
||||
|
||||
# Put a discount of 30% on all SO lines.
|
||||
wizard = self.env['sale.order.discount'].create({
|
||||
'sale_order_id': so.id,
|
||||
'discount_type': 'sol_discount',
|
||||
'discount_percentage': 0.3,
|
||||
})
|
||||
wizard.action_apply_discount()
|
||||
|
||||
self.assertRecordValues(so.order_line, [
|
||||
{'name': 'line_1', 'discount': 30.0},
|
||||
{'name': 'line_2', 'discount': 30.0},
|
||||
])
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 2100.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 2100.0,
|
||||
}])
|
||||
|
||||
# Use the same wizard to clear the discount.
|
||||
wizard.discount_percentage = 0.0
|
||||
wizard.action_apply_discount()
|
||||
|
||||
self.assertRecordValues(so.order_line, [
|
||||
{'name': 'line_1', 'discount': 0.0},
|
||||
{'name': 'line_2', 'discount': 0.0},
|
||||
])
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 3000.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 3000.0,
|
||||
}])
|
||||
|
||||
# Try to put a percentage higher than 100%.
|
||||
with self.assertRaises(ValidationError):
|
||||
wizard.discount_percentage = 110.0
|
||||
|
||||
def test_cumulative_global_discounts(self):
|
||||
product = self.company_data['product_order_cost']
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [Command.create({
|
||||
'name': 'line_1',
|
||||
'product_id': product.id,
|
||||
'price_unit': 2000.0,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 2000.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 2000.0,
|
||||
}])
|
||||
|
||||
# Put a discount of 25%.
|
||||
wizard = self.env['sale.order.discount'].create({
|
||||
'sale_order_id': so.id,
|
||||
'discount_type': 'so_discount',
|
||||
'discount_percentage': 0.25,
|
||||
})
|
||||
wizard.action_apply_discount()
|
||||
|
||||
self.assertRecordValues(so.order_line, [
|
||||
{'price_unit': 2000.0},
|
||||
{'price_unit': -500.0},
|
||||
])
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 1500.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 1500.0,
|
||||
}])
|
||||
|
||||
# Put another discount of 10%.
|
||||
wizard.discount_percentage = 0.10
|
||||
wizard.action_apply_discount()
|
||||
|
||||
self.assertRecordValues(so.order_line, [
|
||||
{'price_unit': 2000.0},
|
||||
{'price_unit': -500.0},
|
||||
{'price_unit': -150.0},
|
||||
])
|
||||
self.assertRecordValues(so, [{
|
||||
'amount_untaxed': 1350.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_total': 1350.0,
|
||||
}])
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
from odoo.addons.account.tests.test_taxes_tax_totals_summary import TestTaxesTaxTotalsSummary
|
||||
from odoo.addons.sale.tests.common import TestTaxCommonSale
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxesTaxTotalsSummarySale(TestTaxCommonSale, TestTaxesTaxTotalsSummary):
|
||||
|
||||
def test_taxes_l10n_in_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_taxes_l10n_in():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_taxes_l10n_br_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_taxes_l10n_br():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_taxes_l10n_be_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_taxes_l10n_be():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_taxes_l10n_mx_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_taxes_l10n_mx():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_reverse_charge_taxes_1_generic_helpers(self):
|
||||
for document, expected_values in self._test_reverse_charge_taxes_1():
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_reverse_charge_taxes_2_generic_helpers(self):
|
||||
for document, expected_values in self._test_reverse_charge_taxes_2():
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_mixed_combined_standalone_taxes_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_mixed_combined_standalone_taxes():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_preceding_subtotal_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_preceding_subtotal():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_preceding_subtotal_with_tax_group_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_preceding_subtotal_with_tax_group():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_preceding_subtotal_with_include_base_amount_sale_orders(self):
|
||||
document, expected_values = self._test_preceding_subtotal_with_include_base_amount()
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_reverse_charge_percent_tax_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_reverse_charge_percent_tax():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
self.assertRecordValues(sale_order.order_line, [{
|
||||
'price_subtotal': expected_values['total_amount_currency'],
|
||||
'price_total': expected_values['total_amount_currency'],
|
||||
}])
|
||||
|
||||
def test_reverse_charge_division_tax_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_reverse_charge_division_tax():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
self.assertRecordValues(sale_order.order_line, [{
|
||||
'price_subtotal': expected_values['total_amount_currency'],
|
||||
'price_total': expected_values['total_amount_currency'],
|
||||
}])
|
||||
|
||||
def test_discount_with_round_globally_sale_orders(self):
|
||||
for test_index, document, expected_values in self._test_discount_with_round_globally():
|
||||
with self.subTest(test_index=test_index):
|
||||
sale_order = self.convert_document_to_sale_order(document)
|
||||
self.assert_sale_order_tax_totals_summary(sale_order, expected_values)
|
||||
|
||||
def test_apply_mixed_epd_discount(self):
|
||||
"""
|
||||
When applying an epd - mixed payment term, the tax should be computed based on the discounted untaxed amount.
|
||||
"""
|
||||
tax_a = self.percent_tax(15.0)
|
||||
early_payment_term = self.env['account.payment.term'].create({
|
||||
'name': "early_payment_term",
|
||||
'early_pay_discount_computation': 'mixed',
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 10,
|
||||
'early_discount': True,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 20,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'payment_term_id': early_payment_term.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [Command.set(tax_a.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assert_sale_order_tax_totals_summary(
|
||||
sale_order,
|
||||
{
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 13.5,
|
||||
'total_amount_currency': 113.5,
|
||||
},
|
||||
soft_checking=True,
|
||||
)
|
||||
|
||||
def test_apply_mixed_epd_discount_fixed_tax(self):
|
||||
"""
|
||||
When applying an epd - mixed payment term, the fixed tax amount should be the same.
|
||||
"""
|
||||
tax_a = self.fixed_tax(20.0)
|
||||
early_payment_term = self.env['account.payment.term'].create({
|
||||
'name': "early_payment_term",
|
||||
'early_pay_discount_computation': 'mixed',
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 10,
|
||||
'early_discount': True,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 20,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'payment_term_id': early_payment_term.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [Command.set(tax_a.ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assert_sale_order_tax_totals_summary(
|
||||
sale_order,
|
||||
{
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 20.0,
|
||||
'total_amount_currency': 120.0,
|
||||
},
|
||||
soft_checking=True,
|
||||
)
|
||||
|
||||
def test_apply_mixed_epd_discount_percent_and_fixed_tax(self):
|
||||
"""
|
||||
When applying an epd - mixed payment term, the percent tax should be computed based on the discounted untaxed amount.
|
||||
"""
|
||||
tax_a = self.percent_tax(15.0)
|
||||
tax_b = self.fixed_tax(20.0)
|
||||
early_payment_term = self.env['account.payment.term'].create({
|
||||
'name': "early_payment_term",
|
||||
'early_pay_discount_computation': 'mixed',
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 10,
|
||||
'early_discount': True,
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 100,
|
||||
'nb_days': 20,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'payment_term_id': early_payment_term.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [Command.set((tax_a + tax_b).ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assert_sale_order_tax_totals_summary(
|
||||
sale_order,
|
||||
{
|
||||
'base_amount_currency': 100.0,
|
||||
'tax_amount_currency': 33.5,
|
||||
'total_amount_currency': 133.5,
|
||||
},
|
||||
soft_checking=True,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue