19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_anglo_saxon_valuation
from . import test_anglo_saxon_valuation_reconciliation
from . import test_anglosaxon_account
@ -8,7 +9,9 @@ from . import test_sale_stock
from . import test_sale_stock_lead_time
from . import test_sale_stock_report
from . import test_sale_order_dates
from . import test_sale_stock_multi_warehouse
from . import test_sale_stock_multicompany
from . import test_sale_stock_accrued_entries
from . import test_sale_stock_access_rights
from . import test_create_perf
from . import test_packaging_tours

View file

@ -0,0 +1,57 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.addons.product.tests.common import ProductVariantsCommon
from odoo.addons.sale.tests.common import TestSaleCommon
class TestSaleStockCommon(TestSaleCommon, ProductVariantsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.warehouse_3_steps_pull = cls.env['stock.warehouse'].create({
'name': 'Warehouse 3 steps',
'code': '3S',
'delivery_steps': 'pick_pack_ship',
})
delivery_route_3 = cls.warehouse_3_steps_pull.delivery_route_id
delivery_route_3.rule_ids[0].write({
'location_dest_id': delivery_route_3.rule_ids[1].location_src_id.id,
})
delivery_route_3.rule_ids[1].write({'action': 'pull'})
delivery_route_3.rule_ids[2].write({'action': 'pull'})
cls.account_income = cls.company.income_account_id
def _inv_adj_two_units(self, product):
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id, # tracking serial
'inventory_quantity': 2,
'location_id': self.stock_location.id,
}).action_apply_inventory()
def _so_deliver(self, product, quantity=1, price=1, picking=True, partner=None, date_order=None, currency=None):
if partner is None:
partner = self.owner
vals = {
'partner_id': partner.id,
'warehouse_id': self.warehouse.id,
'order_line': [Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': quantity,
'price_unit': price,
'tax_ids': False,
})],
}
if date_order:
vals['date_order'] = date_order
if currency:
vals['currency_id'] = currency.id
so = self.env['sale.order'].sudo().create(vals)
so.action_confirm()
if picking:
so.picking_ids.move_ids.write({'quantity': quantity, 'picked': True})
so.picking_ids.button_validate()
return so

View file

@ -1,205 +1,183 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.addons.stock_account.tests.common import TestStockValuationCommon
from odoo.addons.sale_stock.tests.common import TestSaleStockCommon
from odoo.tests import Form, tagged
class TestValuationReconciliationCommon(ValuationReconciliationTestCommon):
@tagged('post_install', '-at_install')
class TestValuationReconciliationCommon(TestStockValuationCommon, TestSaleStockCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# Set the invoice_policy to delivery to have an accurate COGS entry.
cls.test_product_delivery.invoice_policy = 'delivery'
def _create_sale(self, product, date, quantity=1.0):
rslt = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'order_line': [
(0, 0, {
'name': product.name,
'product_id': product.id,
'product_uom_qty': quantity,
'product_uom': product.uom_po_id.id,
'price_unit': 66.0,
})],
'date_order': date,
def setUpClass(cls):
super().setUpClass()
cls.other_currency = cls.setup_other_currency('EUR')
cls.product_standard_auto = cls.env['product.product'].create({
'name': 'Test product template invoiced on delivery',
'standard_price': 42.0,
'is_storable': True,
'categ_id': cls.category_standard_auto.id,
'uom_id': cls.uom.id,
'invoice_policy': 'delivery',
})
rslt.action_confirm()
return rslt
def _create_invoice_for_so(self, sale_order, product, date, quantity=1.0):
rslt = self.env['account.move'].create({
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'move_type': 'out_invoice',
'invoice_date': date,
'invoice_line_ids': [(0, 0, {
'name': 'test line',
'account_id': self.company_data['default_account_revenue'].id,
'price_unit': 66.0,
'quantity': quantity,
'discount': 0.0,
'product_uom_id': product.uom_id.id,
'product_id': product.id,
'sale_line_ids': [(6, 0, sale_order.order_line.ids)],
})],
cls.product_standard_auto_2 = cls.env['product.product'].create({
'name': 'Test product template invoiced on delivery 2',
'standard_price': 42.0,
'is_storable': True,
'categ_id': cls.category_standard_auto.id,
'uom_id': cls.uom.id,
'invoice_policy': 'delivery',
})
sale_order.invoice_ids += rslt
return rslt
def _process_pickings(self, pickings, quantity=None):
for move in pickings.move_ids:
move._action_assign()
if quantity is not None:
move.write({'quantity': quantity, 'picked': True})
else:
move.write({'quantity': move.product_uom_qty, 'picked': True})
pickings.button_validate()
def _set_initial_stock_for_product(self, product):
move1 = self.env['stock.move'].create({
'name': 'Initial stock',
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_id': product.id,
'product_uom': product.uom_id.id,
'product_uom_qty': 11,
'price_unit': 13,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 11
move1._action_done()
@tagged('post_install', '-at_install')
class TestValuationReconciliation(TestValuationReconciliationCommon):
def test_shipment_invoice(self):
""" Tests the case into which we send the goods to the customer before
making the invoice
"""
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
test_product = self.product_standard_auto
self._make_in_move(test_product, 11, 13)
sale_order = self._create_sale(test_product, '2108-01-01')
sale_order = self._so_deliver(test_product, quantity=1, price=66.0, picking=False, partner=self.partner_b, date_order='2108-01-01', currency=self.other_currency)
self._process_pickings(sale_order.picking_ids)
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12')
invoice.action_post()
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
self.check_reconciliation(invoice, picking, operation='sale')
self._create_invoice(test_product, quantity=1, price_unit=66.0, invoice_date='2018-02-12', currency_id=self.other_currency.id, account_id=self.account_income.id)
amls = self.env['account.move.line'].search([('product_id', '=', test_product.id)])
self.assertRecordValues(amls, [
{'debit': 0.0, 'credit': 66.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 42.0, 'account_id': self.account_stock_valuation.id},
{'debit': 42.0, 'credit': 0.0, 'account_id': self.account_expense.id},
])
def test_invoice_shipment(self):
""" Tests the case into which we make the invoice first, and then send
the goods to our customer.
"""
test_product = self.test_product_delivery
#since the invoice come first, the COGS will use the standard price on product
self.test_product_delivery.standard_price = 13
self._set_initial_stock_for_product(test_product)
test_product = self.product_standard_auto
# since the invoice come first, the COGS will use the standard price on product
self.product_standard_auto.standard_price = 13
self._make_in_move(test_product, 11, 13)
sale_order = self._create_sale(test_product, '2018-01-01')
sale_order = self._so_deliver(test_product, quantity=1, price=66.0, picking=False, partner=self.partner_b, date_order='2018-01-01', currency=self.other_currency)
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
invoice.action_post()
invoice = self._create_invoice(test_product, quantity=1, price_unit=66.0, invoice_date='2018-02-03', currency_id=self.other_currency.id, account_id=self.account_income.id)
self._process_pickings(sale_order.picking_ids)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
self.check_reconciliation(invoice, picking, operation='sale')
amls = self.env['account.move.line'].search([('product_id', '=', test_product.id)])
self.assertRecordValues(amls, [
{'debit': 0.0, 'credit': 66.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 13.0, 'account_id': self.account_stock_valuation.id},
{'debit': 13.0, 'credit': 0.0, 'account_id': self.account_expense.id},
])
#return the goods and refund the invoice
stock_return_picking_form = Form(self.env['stock.return.picking']
.with_context(active_ids=picking.ids, active_id=picking.ids[0],
.with_context(active_ids=sale_order.picking_ids.ids, active_id=sale_order.picking_ids.ids[0],
active_model='stock.picking'))
stock_return_picking = stock_return_picking_form.save()
stock_return_picking.product_return_moves.quantity = 1.0
stock_return_picking_action = stock_return_picking.create_returns()
stock_return_picking_action = stock_return_picking.action_create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.action_assign()
return_pick.move_ids.quantity_done = 1
return_pick.move_ids.write({'quantity': 1, 'picked': True})
return_pick._action_done()
refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model='account.move', active_ids=[invoice.id]).create({
'reason': 'test_invoice_shipment_refund',
'refund_method': 'cancel',
'journal_id': invoice.journal_id.id,
})
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
new_invoice = self.env['account.move'].browse(refund_invoice_wiz.modify_moves()['res_id'])
self.assertEqual(invoice.payment_state, 'reversed', "Invoice should be in 'reversed' state.")
self.assertEqual(refund_invoice.payment_state, 'paid', "Refund should be in 'paid' state.")
self.check_reconciliation(refund_invoice, return_pick, operation='sale')
self.assertEqual(invoice.reversal_move_ids.payment_state, 'paid', "Refund should be in 'paid' state.")
self.assertEqual(new_invoice.state, 'draft', "New invoice should be in 'draft' state.")
def test_multiple_shipments_invoices(self):
""" Tests the case into which we deliver part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
"""
test_product = self.test_product_delivery
self._set_initial_stock_for_product(test_product)
test_product = self.product_standard_auto
self._make_in_move(test_product, 11, 13)
sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
sale_order = self._so_deliver(test_product, quantity=5, price=66.0, picking=False, partner=self.partner_b, date_order='2018-01-01', currency=self.other_currency)
self._process_pickings(sale_order.picking_ids, quantity=2.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1)
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03', quantity=3)
invoice.action_post()
self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale')
invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12', quantity=2)
invoice2.action_post()
self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale')
self._create_invoice(test_product, quantity=3, price_unit=66.0, invoice_date='2018-02-03', currency_id=self.other_currency.id, account_id=self.account_income.id)
self._create_invoice(test_product, quantity=2, price_unit=66.0, invoice_date='2018-03-12', currency_id=self.other_currency.id, account_id=self.account_income.id)
self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1)
self.check_reconciliation(invoice2, picking, operation='sale')
# Final check, everything should be reconciled
amls = self.env['account.move.line'].search([('product_id', '=', test_product.id)])
self.assertRecordValues(amls, [
{'debit': 0.0, 'credit': 132.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 84.0, 'account_id': self.account_stock_valuation.id},
{'debit': 84.0, 'credit': 0.0, 'account_id': self.account_expense.id},
{'debit': 0.0, 'credit': 198.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 126.0, 'account_id': self.account_stock_valuation.id},
{'debit': 126.0, 'credit': 0.0, 'account_id': self.account_expense.id},
])
def test_fifo_multiple_products(self):
""" Test Automatic Inventory Valuation with FIFO costs method, 3 products,
2,3,4 out svls and 2 in moves by product. This tests a more complex use case with anglo-saxon accounting.
"""
wh = self.env['stock.warehouse'].search([
('company_id', '=', self.env.company.id),
])
wh = self.warehouse
stock_loc = wh.lot_stock_id
in_type = wh.in_type_id
product_1, product_2, = tuple(self.env['product.product'].create([{
'name': f'P{i}',
# 'categ_id': fifo_categ.id,
'list_price': 10 * i,
'standard_price': 10 * i,
'type': 'product'
} for i in range(1, 3)]))
product_1.categ_id.property_valuation = 'real_time'
product_1.categ_id.property_cost_method = 'fifo'
# give another output account to product_2
product_1 = self.product_fifo_auto
product_1.standard_price = 10
product_1.list_price = 10
# product_2 similar to product_1 but with different output account
product_2 = product_1.copy({'name': 'P2', 'standard_price': 20, 'list_price': 20})
categ_2 = product_1.categ_id.copy()
account_2 = categ_2.property_stock_account_output_categ_id.copy()
categ_2.property_stock_account_output_categ_id = account_2
account_2 = self.env['account.account'].create({
'name': 'Stock Valuation 2',
'code': '100105',
'account_type': 'asset_current',
})
categ_2.property_stock_valuation_account_id = account_2
product_2.categ_id = categ_2
# Create out_svls
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
so = self.env['sale.order'].sudo().create({
'partner_id': self.partner_b.id,
'currency_id': self.other_currency.id,
'order_line': [
(0, 0, {
'name': product.name,
'product_id': product.id,
'product_uom_qty': 2,
'product_uom': product.uom_po_id.id,
'product_uom_id': product.uom_id.id,
'price_unit': 10.0,
}) for product in 2 * [product_1] + [product_2]],
'date_order': '2021-01-01',
})
so.action_confirm()
so.picking_ids.move_ids.quantity_done = 2
so.picking_ids._action_done()
self._process_pickings(so.picking_ids)
self.assertEqual(so.picking_ids.state, 'done')
inv = self.env['account.move'].create({
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_b.id,
'currency_id': self.other_currency.id,
'move_type': 'out_invoice',
'invoice_date': '2021-01-10',
'invoice_line_ids': [(0, 0, {
'name': 'test line',
'account_id': self.company_data['default_account_revenue'].id,
'account_id': self.account_income.id,
'price_unit': 10.0,
'quantity': 2,
'discount': 0.0,
'product_uom_id': line.product_id.uom_id.id,
'product_id': line.product_id.id,
'sale_line_ids': [(6, 0, line.ids)],
}) for line in so.order_line],
@ -207,10 +185,10 @@ class TestValuationReconciliation(TestValuationReconciliationCommon):
so.invoice_ids += inv
inv.action_post()
# Create in_moves for P1/P2 such that the first move compensates the out_svls
# Create in_moves for P1/P2
in_moves = self.env['stock.move'].create([{
'name': 'in %s units @ %s per unit' % (str(quantity), str(product.standard_price)),
'description_picking': '%s-%s' % (str(quantity), str(product)), # to not merge the moves
'description_picking': '%s-%s' % (str(quantity), str(product)),
'product_id': product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': stock_loc.id,
@ -224,27 +202,19 @@ class TestValuationReconciliation(TestValuationReconciliationCommon):
)])
in_moves._action_confirm()
for move in in_moves:
move.quantity_done = move.product_uom_qty
move.quantity = move.product_uom_qty
move.picked = True
in_moves._action_done()
self.assertEqual(product_1.value_svl, -20)
self.assertEqual(product_2.value_svl, 0)
# Check that the correct number of amls have been created and posted
input_aml = self.env['account.move.line'].search([
('account_id', '=', product_1.categ_id.property_stock_account_input_categ_id.id),
], order='date, id')
output1_aml = self.env['account.move.line'].search([
('account_id', '=', product_1.categ_id.property_stock_account_output_categ_id.id),
], order='date, id')
output2_aml = self.env['account.move.line'].search([
('account_id', '=', product_2.categ_id.property_stock_account_output_categ_id.id),
], order='date, id')
valo_aml = self.env['account.move.line'].search([
('account_id', '=', product_1.categ_id.property_stock_valuation_account_id.id),
], order='date, id')
self.assertEqual(len(input_aml), 2)
self.assertEqual(len(output1_aml), 6)
self.assertEqual(len(output2_aml), 4)
self.assertEqual(len(valo_aml), 7)
# All amls should be reconciled
self.assertTrue(all(aml.reconciled for aml in output1_aml + output2_aml))
amls = self.env['account.move.line'].search([('product_id', 'in', [product_1.id, product_2.id])])
self.assertRecordValues(amls, [
{'debit': 0.0, 'credit': 10.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 10.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 10.0, 'account_id': self.account_income.id},
{'debit': 0.0, 'credit': 20.0, 'account_id': self.account_stock_valuation.id},
{'debit': 20.0, 'credit': 0.0, 'account_id': self.account_expense.id},
{'debit': 0.0, 'credit': 20.0, 'account_id': self.account_stock_valuation.id},
{'debit': 20.0, 'credit': 0.0, 'account_id': self.account_expense.id},
{'debit': 0.0, 'credit': 40.0, 'account_id': account_2.id},
{'debit': 40.0, 'credit': 0.0, 'account_id': self.account_expense.id},
])

View file

@ -2,6 +2,7 @@
from odoo.addons.sale_stock.tests.test_anglo_saxon_valuation_reconciliation import TestValuationReconciliationCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAngloSaxonAccounting(TestValuationReconciliationCommon):
@ -10,20 +11,23 @@ class TestAngloSaxonAccounting(TestValuationReconciliationCommon):
Reproduce the flow of creating an invoice from a sale order with company A
and posting the invoice with both companies selected and company B as the main.
"""
company_a_data = self.company_data
company_b_data = self.company_data_2
companies_with_b_first = company_b_data['company'] + company_a_data['company']
product = self.test_product_delivery
# Conflict between account common and stock common file. We should remove the depends on account common.
self.env.user.company_id = self.company
company_a_data = self.company
company_b_data = self._create_company()
companies_with_b_first = company_b_data + company_a_data
self.env.user.company_ids = companies_with_b_first
product = self.product_standard_auto
# set different cost price for the same product in the 2 companies
company_a_standard_price = 20.0
product.with_company(company_a_data['company']).standard_price = company_a_standard_price
product.with_company(company_a_data).standard_price = company_a_standard_price
company_b_standard_price = 10.0
product.with_company(company_b_data['company']).standard_price = company_b_standard_price
product.with_company(company_b_data).standard_price = company_b_standard_price
# create sale order with company A in draft (by default, self.env.user.company_id is company A)
company_a_order = self._create_sale(product, '2021-01-01')
company_a_invoice = self._create_invoice_for_so(company_a_order, product, '2021-01-10')
self._so_deliver(product, quantity=1, price=66.0, picking=False, partner=self.vendor, date_order='2021-01-01')
company_a_invoice = self._create_invoice(product, quantity=1, price_unit=66.0, invoice_date='2021-01-10', post=False)
# Post the invoice from company A with company B
company_a_invoice.with_context(allowed_company_ids=companies_with_b_first.ids).action_post()

View file

@ -1,19 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import functools
import logging
import random
import time
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.fields import Command
from odoo.tests import tagged
from odoo.tests.common import users, warmup
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
_logger = logging.getLogger(__name__)
def prepare(func, /):
"""Prepare data to remove common queries from the count.
Must be run after `warmup` because of the invalidations"""
# prefetch the data linked to the company and its country code to avoid changing
# the query count during l10n tests
@functools.wraps(func)
def test_func(self):
self.env.company.country_id.code
return func(self)
return test_func
@tagged('so_batch_perf')
class TestPERF(TransactionCaseWithUserDemo):
@ -38,6 +51,7 @@ class TestPERF(TransactionCaseWithUserDemo):
@users('admin')
@warmup
@prepare
def test_empty_sale_order_creation_perf(self):
with self.assertQueryCount(admin=34):
self.env['sale.order'].create({
@ -47,6 +61,7 @@ class TestPERF(TransactionCaseWithUserDemo):
@users('admin')
@warmup
@prepare
def test_empty_sales_orders_batch_creation_perf(self):
# + 1 SO insert
# + 1 SO sequence fetch
@ -61,6 +76,7 @@ class TestPERF(TransactionCaseWithUserDemo):
@users('admin')
@warmup
@prepare
def test_dummy_sales_orders_batch_creation_perf(self):
""" Dummy SOlines (notes/sections) should not add any custom queries other than their insert"""
# + 2 SOL (batched) insert
@ -76,12 +92,14 @@ class TestPERF(TransactionCaseWithUserDemo):
@users('admin')
@warmup
@prepare
def test_light_sales_orders_batch_creation_perf_without_taxes(self):
self.env['res.country'].search([]).mapped('code')
self.products[0].taxes_id = [Command.set([])]
# + 2 SQL insert
# + 2 queries to get analytic default tags
# + 9 follower queries ?
with self.assertQueryCount(admin=57):
with self.assertQueryCount(admin=53): # com 46
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
@ -118,16 +136,6 @@ class TestPERF(TransactionCaseWithUserDemo):
# (Seems to be a time-based problem, everytime happening around 10PM)
self._test_complex_sales_orders_batch_creation_perf(1504)
@users('admin')
@warmup
def ___test_complex_sales_orders_batch_creation_perf_with_discount_computation(self):
"""Cover the "complex" logic triggered inside the `_compute_discount`"""
self.env['product.pricelist'].search([]).discount_policy = 'without_discount'
self.env.user.groups_id += self.env.ref('product.group_discount_per_so_line')
# Verify any modification to this count on nightly runbot builds
self._test_complex_sales_orders_batch_creation_perf(1546)
def _test_complex_sales_orders_batch_creation_perf(self, query_count):
MSG = "Model %s, %i records, %s, time %.2f"
@ -155,9 +163,6 @@ class TestPERF(TransactionCaseWithUserDemo):
"""Make sure the price and discounts computation are complexified
and do not gain from any prefetch/batch gains during the price computation
"""
# Enable discounts
self.env['product.pricelist'].search([]).discount_policy = 'without_discount'
self.env.user.groups_id += self.env.ref('product.group_discount_per_so_line')
vals_list = [{
"partner_id": self.partners[i].id,

View file

@ -0,0 +1,30 @@
from odoo import Command
from odoo.tests import HttpCase, tagged
from odoo.tools import mute_logger
@tagged('-at_install', 'post_install')
class TestPackagingTours(HttpCase):
def _get_product_url(self, product_id):
return '/odoo/action-stock.product_template_action_product/%s' % (product_id)
def test_barcode_duplication_error(self):
""" Test the barcode duplication error when creating a new product with an existing barcode """
product_a = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'none',
'uom_id': self.env.ref('uom.product_uom_unit').id,
'uom_ids': [Command.link(self.env.ref('uom.product_uom_pack_6').id)],
'product_uom_ids': [Command.create({
'barcode': 'test-1234',
'uom_id': self.env.ref('uom.product_uom_pack_6').id,
})]
})
url = self._get_product_url(product_a.product_tmpl_id.id)
self.env['res.config.settings'].create({
'group_uom': True,
}).execute()
with mute_logger('odoo.sql_db', 'odoo.http'):
self.start_tour(url, 'test_barcode_duplication_error', login='admin', timeout=60)

View file

@ -1,10 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from datetime import timedelta
from freezegun import freeze_time
from odoo import fields
from odoo.tests import common, tagged
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import (
ValuationReconciliationTestCommon,
)
@tagged('post_install', '-at_install')
@ -16,19 +22,19 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
product_A = Product.create({
'name': 'Product A',
'type': 'product',
'is_storable': True,
'sale_delay': 5,
'uom_id': 1,
})
product_B = Product.create({
'name': 'Product B',
'type': 'product',
'is_storable': True,
'sale_delay': 10,
'uom_id': 1,
})
product_C = Product.create({
'name': 'Product C',
'type': 'product',
'is_storable': True,
'sale_delay': 15,
'uom_id': 1,
})
@ -37,13 +43,13 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
self.env['stock.quant']._update_available_quantity(product_B, self.company_data['default_warehouse'].lot_stock_id, 10)
self.env['stock.quant']._update_available_quantity(product_C, self.company_data['default_warehouse'].lot_stock_id, 10)
sale_order = self.env['sale.order'].create({
sale_order = self.env['sale.order'].sudo().create({
'partner_id': self.env['res.partner'].create({'name': 'A Customer'}).id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': product_A.name, 'product_id': product_A.id, 'customer_lead': product_A.sale_delay, 'product_uom_qty': 5}),
(0, 0, {'name': product_B.name, 'product_id': product_B.id, 'customer_lead': product_B.sale_delay, 'product_uom_qty': 5}),
(0, 0, {'name': product_C.name, 'product_id': product_C.id, 'customer_lead': product_C.sale_delay, 'product_uom_qty': 5})
Command.create({'product_id': product_A.id, 'product_uom_qty': 5}),
Command.create({'product_id': product_B.id, 'product_uom_qty': 5}),
Command.create({'product_id': product_C.id, 'product_uom_qty': 5})
],
})
@ -82,8 +88,7 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
# Check effective date, it should be date on which the first shipment successfully delivered to customer
picking = sale_order.picking_ids[0]
for ml in picking.move_line_ids:
ml.qty_done = ml.reserved_uom_qty
picking.move_ids.picked = True
picking._action_done()
self.assertEqual(picking.state, 'done', "Picking not processed correctly!")
self.assertEqual(fields.Date.today(), sale_order.effective_date.date(), "Wrong effective date on sale order!")
@ -92,17 +97,17 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
# In order to test the Commitment Date feature in Sales Orders in Odoo,
# I copy a demo Sales Order with committed Date on 2010-07-12
new_order = self.env['sale.order'].create({
new_order = self.env['sale.order'].sudo().create({
'partner_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
'order_line': [(0, 0, {
'name': "A product",
'product_id': self.env['product.product'].create({
'name': 'A product',
'type': 'product',
}).id,
'product_uom_qty': 1,
'price_unit': 750,
})],
'order_line': [
Command.create({
'product_id': self.env['product.product'].create({
'name': 'A product',
'is_storable': True,
}).id,
'price_unit': 750,
})
],
'commitment_date': '2010-07-12',
})
# I confirm the Sales Order.
@ -113,3 +118,94 @@ class TestSaleExpectedDate(ValuationReconciliationTestCommon):
right_date = commitment_date - security_delay
for line in new_order.order_line:
self.assertEqual(line.move_ids[0].date, right_date, "The expected date for the Stock Move is wrong")
@freeze_time('2025-10-10')
def test_expected_date_with_storable_product(self):
''' This test ensures the expected date is computed based on only goods(consu) products.
It's avoiding computation for non-goods products.
'''
sale_delay = 10.0
self.product.sale_delay = sale_delay
# Create a sale order with a consu product.
sale_order = self.env['sale.order'].sudo().create({
'partner_id': self.partner.id,
'order_line': [Command.create({
'product_id': self.product.id,
'product_uom_qty': 1000,
})],
})
# Ensure that expected date is correctly computed based on the consu product's sale delay.
self.assertEqual(sale_order.expected_date, fields.Datetime.now() + timedelta(days=sale_delay))
# Add a service product and ensure the expected date remains unchanged.
sale_order.write({
'order_line': [Command.create({
'product_id': self.service_product.id,
'product_uom_qty': 1000,
})],
})
self.assertEqual(sale_order.expected_date, fields.Datetime.now() + timedelta(days=sale_delay))
def test_invoice_delivery_date(self):
self.env['stock.quant']._update_available_quantity(
self.test_product_order,
self.company_data['default_warehouse'].lot_stock_id,
75.0,
)
order = self.env['sale.order'].sudo().create({
'partner_id': self.partner_a.id,
'picking_policy': 'one',
'order_line': [Command.create({
'product_id': self.test_product_order.id,
'product_uom_qty': 100.0,
})],
})
order.action_confirm()
picking_1 = order.picking_ids
picking_1.move_ids.picked = True
invoice = order._create_invoices()
self.assertFalse(invoice.delivery_date)
picking_1._action_done()
self.assertTrue(order.effective_date, "Effective date should exist after done picking")
effective_date = order.effective_date.date()
self.assertEqual(
invoice.delivery_date, effective_date,
"Default invoice delivery date should equal effective date",
)
self.env['stock.quant']._update_available_quantity(
self.test_product_order,
self.company_data['default_warehouse'].lot_stock_id,
25.0,
)
with freeze_time(effective_date + timedelta(days=3)):
custom_delivery_date = fields.Date.today()
picking_2 = (order.picking_ids - picking_1).ensure_one()
picking_2.move_ids.write({'quantity': 25.0, 'picked': True})
picking_2._action_done()
self.assertEqual(
invoice.delivery_date, effective_date,
"Invoice delivery date should default to earliest picking date",
)
product_line = invoice.line_ids[0]
invoice.write({
'delivery_date': custom_delivery_date,
'line_ids': [Command.update(product_line.id, {'quantity': 0.0})],
})
product_line.quantity += 75.0
self.assertEqual(
invoice.delivery_date, custom_delivery_date,
"Custom invoice delivery shouldn't change after line change",
)
invoice.action_post()
self.assertEqual(
invoice.delivery_date, custom_delivery_date,
"Custom invoice delivery shouldn't change posting invoice",
)
invoice.button_draft()
self.assertEqual(
invoice.delivery_date, custom_delivery_date,
"Custom invoice delivery shouldn't change resetting to draft invoice",
)

View file

@ -1,35 +1,72 @@
# -*- coding: utf-8 -*-
from odoo import fields, Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from freezegun.api import freeze_time
from odoo import fields
from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tests import Form, tagged
from odoo.addons.sale.tests.common import TestSaleCommon
@tagged('post_install', '-at_install')
class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
class TestAccruedStockSaleOrders(TestSaleCommon):
def _make_in_move(self,
product,
quantity,
unit_cost=None,
):
""" Helper to create and validate a receipt move.
:param product: Product to move
:param quantity: Quantity to move
:param unit_cost: Price unit
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
receipt_type = warehouse.in_type_id
product_qty = quantity
move_vals = {
'product_id': product.id,
'location_id': receipt_type.default_location_src_id.id,
'location_dest_id': receipt_type.default_location_dest_id.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': quantity,
'picking_type_id': receipt_type.id,
}
if unit_cost:
move_vals['value_manual'] = unit_cost * product_qty
move_vals['price_unit'] = unit_cost
else:
move_vals['value_manual'] = product.standard_price * product_qty
in_move = self.env['stock.move'].create(move_vals)
in_move._action_confirm()
in_move._action_assign()
in_move.picked = True
in_move._action_done()
return in_move
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
uom_unit = cls.env.ref('uom.product_uom_unit')
cls.product_order = cls.env['product.product'].create({
def setUpClass(cls):
super().setUpClass()
product = cls.env['product.product'].create({
'name': "Product",
'list_price': 30.0,
'type': 'consu',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'uom_id': cls.uom_unit.id,
'invoice_policy': 'delivery',
})
cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': cls.partner_a.id,
'order_line': [
Command.create({
'name': cls.product_order.name,
'product_id': cls.product_order.id,
'product_id': product.id,
'product_uom_qty': 10.0,
'product_uom': cls.product_order.uom_id.id,
'price_unit': cls.product_order.list_price,
'tax_id': False,
'tax_ids': False,
})
]
})
@ -40,17 +77,16 @@ class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
def test_sale_stock_accruals(self):
# deliver 2 on 2020-01-02
pick = self.sale_order.picking_ids
pick.move_ids.write({'quantity_done': 2})
pick.move_ids.write({'quantity': 2, 'picked': True})
pick.button_validate()
wiz_act = pick.button_validate()
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
wiz.process()
Form.from_action(self.env, wiz_act).save().process()
pick.move_ids.write({'date': fields.Date.to_date('2020-01-02')})
# deliver 3 on 2020-01-06
pick = pick.copy()
pick.move_ids.write({'quantity_done': 3})
wiz_act = pick.button_validate()
pick.move_ids.write({'quantity': 3, 'picked': True})
pick.button_validate()
pick.move_ids.write({'date': fields.Date.to_date('2020-01-06')})
wizard = self.env['account.accrued.orders.wizard'].with_context({
@ -89,11 +125,9 @@ class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
def test_sale_stock_invoiced_accrued_entries(self):
# deliver 2 on 2020-01-02
pick = self.sale_order.picking_ids
pick.move_ids.write({'quantity_done': 2})
pick.move_ids.write({'quantity': 2, 'picked': True})
pick.button_validate()
wiz_act = pick.button_validate()
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
wiz.process()
Form.from_action(self.env, pick.button_validate()).save().process()
pick.move_ids.write({'date': fields.Date.to_date('2020-01-02')})
# invoice on 2020-01-04
@ -103,8 +137,8 @@ class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
# deliver 3 on 2020-01-06
pick = pick.copy()
pick.move_ids.write({'quantity_done': 3})
wiz_act = pick.button_validate()
pick.move_ids.write({'quantity': 3, 'picked': True})
pick.button_validate()
pick.move_ids.write({'date': fields.Date.to_date('2020-01-06')})
# invoice on 2020-01-08
@ -149,3 +183,265 @@ class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
wizard.date = fields.Date.to_date('2020-01-09')
with self.assertRaises(UserError):
wizard.create_entries()
def test_accrued_order_in_anglo_saxon_standard_perpetual(self):
""" Ensure the COGS accrual lines are correctly computed."""
# Create a product using anglox-saxon valuation.
product_category = self.env['product.category'].create({
'name': 'Test Category',
'property_account_income_categ_id': self.account_revenue.id,
'property_account_expense_categ_id': self.account_expense.id,
'property_valuation': 'real_time',
})
account_variation = product_category.property_stock_valuation_account_id.account_stock_variation_id
anglo_saxon_product = self.env['product.product'].create({
'name': "Saxy Product",
'categ_id': product_category.id,
'invoice_policy': 'order',
'is_storable': True,
'list_price': 120,
'standard_price': 80,
'uom_id': self.uom_unit.id,
})
# Case 1.: SO with more delivered quantities than invoiced quantities.
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': anglo_saxon_product.id,
'product_uom_qty': 1,
'price_unit': 135, # Must be different than standard cost.
'tax_ids': False,
})
]
})
sale_order_1.action_confirm()
sale_order_1.picking_ids.move_ids.write({'quantity': 1, 'picked': True})
sale_order_1.picking_ids.button_validate()
# Use accrued order wizard and check generated values.
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order_1.id],
}).create({
'account_id': self.account_expense.id,
'date': fields.Date.today(),
})
account_move_domain = wizard.create_entries()['domain']
account_move = self.env['account.move'].search(account_move_domain)
self.assertRecordValues(account_move.line_ids.sorted('id'), [
# Accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 135},
{'account_id': self.account_expense.id, 'debit': 135, 'credit': 0},
{'account_id': account_variation.id, 'debit': 0, 'credit': 80},
{'account_id': self.account_expense.id, 'debit': 80, 'credit': 0},
# Reversal of accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 135, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 135},
{'account_id': account_variation.id, 'debit': 80, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 80},
])
# Case 2.: SO with more invoiced quantities than delivered quantities.
sale_order_2 = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': anglo_saxon_product.id,
'product_uom_qty': 1,
'tax_ids': False,
})
]
})
sale_order_2.action_confirm()
invoice = sale_order_2._create_invoices()
invoice.line_ids.price_unit = 140 # Must be different than SO line unit cost.
invoice.action_post()
# Use accrued order wizard and check generated values.
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order_2.id],
}).create({
'account_id': self.account_expense.id,
'date': fields.Date.today(),
})
account_move_domain = wizard.create_entries()['domain']
account_move = self.env['account.move'].search(account_move_domain)
self.assertRecordValues(account_move.line_ids.sorted('id'), [
# Accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 140, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 140},
{'account_id': account_variation.id, 'debit': 80, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 80},
# Reversal of accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 140},
{'account_id': self.account_expense.id, 'debit': 140, 'credit': 0},
{'account_id': account_variation.id, 'debit': 0, 'credit': 80},
{'account_id': self.account_expense.id, 'debit': 80, 'credit': 0},
])
def test_accrued_order_in_anglo_saxon_avco_perpetual(self):
""" Ensure the COGS accrual lines are correctly computed for AVCO costing method product."""
# Create a product using anglox-saxon valuation.
product_category = self.env['product.category'].create({
'name': 'Test AVCO Category',
'property_account_income_categ_id': self.account_revenue.id,
'property_account_expense_categ_id': self.account_expense.id,
'property_valuation': 'real_time',
'property_cost_method': 'average',
})
account_variation = product_category.property_stock_valuation_account_id.account_stock_variation_id
# Set the product in the past so its `product.value` won't be considered as the most recent one.
with freeze_time(fields.Datetime.now() - timedelta(seconds=10)):
avco_product = self.env['product.product'].create({
'name': "AVCO Product",
'categ_id': product_category.id,
'invoice_policy': 'order',
'is_storable': True,
'standard_price': 0,
'uom_id': self.uom_unit.id,
})
self._make_in_move(avco_product, 10, 10)
self._make_in_move(avco_product, 10, 20)
# Create a SO for 10 units, deliver 7 units and invoice 5 units.
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': avco_product.id,
'product_uom_qty': 10,
'price_unit': 35,
'tax_ids': False,
})
]
})
sale_order_1.action_confirm()
# Deliver 7 / 10 units.
sale_order_1.picking_ids.move_ids.write({'quantity': 5, 'picked': True})
backorder_wizard = Form.from_action(self.env, sale_order_1.picking_ids.button_validate())
backorder_wizard.save().process()
backorder_delivery = sale_order_1.picking_ids.filtered(lambda pick: pick.state == 'assigned')
backorder_delivery.move_ids.write({'quantity': 2, 'picked': True})
backorder_wizard = Form.from_action(self.env, sale_order_1.picking_ids.button_validate())
backorder_wizard.save().process()
# Invoice 5 / 10 units.
invoice = sale_order_1._create_invoices()
invoice.line_ids.quantity = 5
invoice.action_post()
# Use accrued order wizard and check generated values.
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order_1.id],
}).create({
'account_id': self.account_expense.id,
'date': fields.Date.today(),
})
account_move_domain = wizard.create_entries()['domain']
account_move = self.env['account.move'].search(account_move_domain)
self.assertRecordValues(account_move.line_ids.sorted('id'), [
# Accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 70},
{'account_id': self.account_expense.id, 'debit': 70, 'credit': 0},
{'account_id': account_variation.id, 'debit': 0, 'credit': 30},
{'account_id': self.account_expense.id, 'debit': 30, 'credit': 0},
# Reversal of accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 70, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 70},
{'account_id': account_variation.id, 'debit': 30, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 30},
])
def test_accrued_order_in_anglo_saxon_fifo_perpetual(self):
""" Ensure the COGS accrual lines are correctly computed for FIFO costing method product."""
# Create a product using anglox-saxon valuation.
product_category = self.env['product.category'].create({
'name': 'Test FIFO Category',
'property_account_income_categ_id': self.account_revenue.id,
'property_account_expense_categ_id': self.account_expense.id,
'property_valuation': 'real_time',
'property_cost_method': 'fifo',
})
account_variation = product_category.property_stock_valuation_account_id.account_stock_variation_id
fifo_product = self.env['product.product'].create({
'name': "FIFO Product",
'categ_id': product_category.id,
'invoice_policy': 'order',
'is_storable': True,
'standard_price': 15,
'uom_id': self.uom_unit.id,
})
self._make_in_move(fifo_product, 10, 10)
self._make_in_move(fifo_product, 10, 20)
# Create a SO for 10 units, deliver 7 units and invoice 5 units.
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': fifo_product.id,
'product_uom_qty': 20,
'price_unit': 30,
'tax_ids': False,
})
]
})
sale_order_1.action_confirm()
# Deliver 17 / 20 units.
sale_order_1.picking_ids.move_ids.write({'quantity': 17, 'picked': True})
backorder_wizard = Form.from_action(self.env, sale_order_1.picking_ids.button_validate())
backorder_wizard.save().process()
# Invoice 5 / 20 units.
invoice = sale_order_1._create_invoices()
invoice.line_ids.quantity = 5
invoice.line_ids.price_unit = 30
invoice.action_post()
# Use accrued order wizard and check generated values.
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order_1.id],
}).create({
'account_id': self.account_expense.id,
'date': fields.Date.today(),
})
account_move_domain = wizard.create_entries()['domain']
account_move = self.env['account.move'].search(account_move_domain)
self.assertRecordValues(account_move.line_ids.sorted('id'), [
# Accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 360},
{'account_id': self.account_expense.id, 'debit': 360, 'credit': 0},
{'account_id': account_variation.id, 'debit': 0, 'credit': 169.41},
{'account_id': self.account_expense.id, 'debit': 169.41, 'credit': 0},
# Reversal of accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 360, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 360},
{'account_id': account_variation.id, 'debit': 169.41, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 169.41},
])
# Delivery 3 more units (20 / 20 units.)
backorder_delivery = sale_order_1.picking_ids.filtered(lambda pick: pick.state == 'assigned')
backorder_delivery.button_validate()
# Use accrued order wizard and check generated values.
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': [sale_order_1.id],
}).create({
'account_id': self.account_expense.id,
'date': fields.Date.today(),
})
account_move_domain = wizard.create_entries()['domain']
account_move = self.env['account.move'].search(account_move_domain)
self.assertRecordValues(account_move.line_ids.sorted('id'), [
# Accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 450},
{'account_id': self.account_expense.id, 'debit': 450, 'credit': 0},
{'account_id': account_variation.id, 'debit': 0, 'credit': 229.41},
{'account_id': self.account_expense.id, 'debit': 229.41, 'credit': 0},
# Reversal of accrued revenues entries.
{'account_id': self.account_revenue.id, 'debit': 450, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 450},
{'account_id': account_variation.id, 'debit': 229.41, 'credit': 0},
{'account_id': self.account_expense.id, 'debit': 0, 'credit': 229.41},
])

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.addons.sale_stock.tests.common import TestSaleStockCommon
from odoo import fields
from odoo.tests import tagged
@ -9,11 +9,11 @@ from datetime import timedelta
@tagged('post_install', '-at_install')
class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
class TestSaleStockLeadTime(TestSaleStockCommon, ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
# Update the product_1 with type and Customer Lead Time
cls.test_product_order.sale_delay = 5.0
@ -28,13 +28,11 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
# Create sale order of product_1
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
'order_line': [(0, 0, {
'product_id': self.test_product_order.id,
'product_uom_qty': 10,
'product_uom': self.env.ref('uom.product_uom_unit').id,
})]
})
@ -54,12 +52,11 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
def test_01_product_route_level_delays(self):
""" In order to check schedule dates, set product's Customer Lead Time
and warehouse route's delay."""
# Update warehouse_1 with Outgoing Shippings pick + pack + ship
self.company_data['default_warehouse'].write({'delivery_steps': 'pick_pack_ship'})
# FIXME QUWO: This test no longer works with the current push flow, yet still works with old pull rules.
warehouse = self.warehouse_3_steps_pull
# Set delay on pull rule
for pull_rule in self.company_data['default_warehouse'].delivery_route_id.rule_ids:
for pull_rule in warehouse.delivery_route_id.rule_ids:
pull_rule.write({'delay': 2})
# Create sale order of product_1
@ -67,13 +64,11 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
'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,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
'warehouse_id': warehouse.id,
'order_line': [(0, 0, {'name': self.test_product_order.name,
'product_id': self.test_product_order.id,
'product_uom_qty': 5,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'customer_lead': self.test_product_order.sale_delay})]})
# Confirm our standard sale order
@ -83,19 +78,19 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
self.assertTrue(order.picking_ids, "Pickings should be created.")
# Check schedule date of ship type picking
out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].out_type_id)
out = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.out_type_id)
out_min_date = fields.Datetime.from_string(out.scheduled_date)
out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=out.move_ids[0].rule_id.delay)
self.assertTrue(abs(out_min_date - out_date) <= timedelta(seconds=1), 'Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.')
# Check schedule date of pack type picking
pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pack_type_id)
pack = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.pack_type_id)
pack_min_date = fields.Datetime.from_string(pack.scheduled_date)
pack_date = out_date - timedelta(days=pack.move_ids[0].rule_id.delay)
self.assertTrue(abs(pack_min_date - pack_date) <= timedelta(seconds=1), 'Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
# Check schedule date of pick type picking
pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pick_type_id)
pick = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.pick_type_id)
pick_min_date = fields.Datetime.from_string(pick.scheduled_date)
pick_date = pack_date - timedelta(days=pick.move_ids[0].rule_id.delay)
self.assertTrue(abs(pick_min_date - pick_date) <= timedelta(seconds=1), 'Schedule date of pick type picking should be equal to: Schedule date of pack type picking - pull rule delay.')
@ -104,6 +99,7 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
""" In order to check deadline date propagation, set product's Customer Lead Time
and warehouse route's delay in stock rules"""
# FIXME QUWO: This test no longer works with the current push flow, yet still works with old pull rules.
# Example :
# -> Set Warehouse with Outgoing Shipments : pick + pack + ship
# -> Set Delay : 5 days on stock rules
@ -124,38 +120,34 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
# Update company with Sales Safety Days
self.env.company.security_lead = 2.00
# Update warehouse_1 with Outgoing Shippings pick + pack + ship
self.company_data['default_warehouse'].write({'delivery_steps': 'pick_pack_ship'})
warehouse = self.warehouse_3_steps_pull
# Set delay on pull rule
self.company_data['default_warehouse'].delivery_route_id.rule_ids.write({'delay': 5})
warehouse.delivery_route_id.rule_ids.write({'delay': 5})
# Update the product_1 with type and Customer Lead Time
self.test_product_order.write({'type': 'product', 'sale_delay': 30.0})
self.test_product_order.write({'is_storable': True, 'sale_delay': 30.0})
# Now, create sale order of product_1 with customer_lead set on product
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
'warehouse_id': warehouse.id,
'order_line': [(0, 0, {'name': self.test_product_order.name,
'product_id': self.test_product_order.id,
'product_uom_qty': 5,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'customer_lead': self.test_product_order.sale_delay})]})
# Confirm our standard sale order
order.action_confirm()
# Check the picking crated or not
self.assertTrue(order.picking_ids, "Pickings should be created.")
# Check the pickings creation
self.assertEqual(len(order.picking_ids), 3)
# Check schedule/deadline date of ship type picking
out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].out_type_id)
out = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.out_type_id)
deadline_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=out.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
out.date_deadline, deadline_date, delta=timedelta(seconds=1),
@ -166,7 +158,7 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
msg='Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay - security_lead')
# Check schedule/deadline date of pack type picking
pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pack_type_id)
pack = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.pack_type_id)
pack_scheduled_date = out_scheduled_date - timedelta(days=pack.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
pack.scheduled_date, pack_scheduled_date, delta=timedelta(seconds=1),
@ -177,7 +169,7 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
msg='Deadline date of pack type picking should be equal to: Deadline date of ship type picking - pull rule delay.')
# Check schedule/deadline date of pick type picking
pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pick_type_id)
pick = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.pick_type_id)
pick_scheduled_date = pack_scheduled_date - timedelta(days=pick.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
pick.scheduled_date, pick_scheduled_date, delta=timedelta(seconds=1),
@ -214,7 +206,6 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
"""
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
})
@ -222,7 +213,7 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
order_line = self.env['sale.order.line'].create({
'product_id': self.test_product_order.id,
'product_uom_qty': 10,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
'order_id': order.id,
})

View file

@ -0,0 +1,60 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.addons.sale_stock.tests.common import TestSaleStockCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSaleStockMultiWarehouse(TestSaleStockCommon, ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_a.is_storable = True
cls.warehouse_A = cls.company_data['default_warehouse']
cls.env['stock.quant']._update_available_quantity(cls.product_a, cls.warehouse_A.lot_stock_id, 10)
cls.warehouse_B = cls.env['stock.warehouse'].create({
'name': 'WH B',
'code': 'WHB',
'company_id': cls.env.company.id,
'partner_id': cls.env.company.partner_id.id,
})
cls.env['stock.quant']._update_available_quantity(cls.product_a, cls.warehouse_B.lot_stock_id, 10)
cls.env.user.group_ids |= cls.env.ref('stock.group_stock_user')
cls.env.user.group_ids |= cls.env.ref('stock.group_stock_multi_locations')
cls.env.user.group_ids |= cls.env.ref('sales_team.group_sale_salesman')
def test_multiple_warehouses_generate_multiple_pickings(self):
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'warehouse_id': self.warehouse_A.id,
'order_line': [
(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_qty': 9,
'price_unit': 1,
'route_ids': [self.warehouse_A.delivery_route_id.id],
}),
(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_qty': 10,
'price_unit': 1,
'route_ids': [self.warehouse_B.delivery_route_id.id],
}),
],
})
so.action_confirm()
# 2 pickings: 1 per warehouse
self.assertEqual(len(so.picking_ids), 2)
# single move per picking
self.assertEqual(len(so.picking_ids[0].move_ids), 1)
self.assertEqual(len(so.picking_ids[1].move_ids), 1)
# pickings comes from the right warehouse
self.assertEqual(so.picking_ids[0].move_ids[0].location_id.warehouse_id, self.warehouse_A)
self.assertEqual(so.picking_ids[1].move_ids[0].location_id.warehouse_id, self.warehouse_B)

View file

@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.fields import Command
from odoo.tests import tagged
@ -9,8 +10,9 @@ from odoo.tests import tagged
class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.company_data_2 = cls.setup_other_company()
cls.warehouse_A = cls.company_data['default_warehouse']
cls.warehouse_A2 = cls.env['stock.warehouse'].create({
@ -21,9 +23,9 @@ class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommo
})
cls.warehouse_B = cls.company_data_2['default_warehouse']
cls.env.user.groups_id |= cls.env.ref('stock.group_stock_user')
cls.env.user.groups_id |= cls.env.ref('stock.group_stock_multi_locations')
cls.env.user.groups_id |= cls.env.ref('sales_team.group_sale_salesman')
cls.env.user.group_ids |= cls.env.ref('stock.group_stock_user')
cls.env.user.group_ids |= cls.env.ref('stock.group_stock_multi_locations')
cls.env.user.group_ids |= cls.env.ref('sales_team.group_sale_salesman')
cls.env.user.with_company(cls.company_data['company']).property_warehouse_id = cls.warehouse_A.id
cls.env.user.with_company(cls.company_data_2['company']).property_warehouse_id = cls.warehouse_B.id
@ -43,9 +45,7 @@ class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommo
'name': product.name,
'product_id': product.id,
'product_uom_qty': 10,
'product_uom': product.uom_id.id,
'price_unit': product.list_price})],
'pricelist_id': self.company_data['default_pricelist'].id,
}
sale_order = self.env['sale.order']
@ -62,9 +62,7 @@ class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommo
'name': product.name,
'product_id': product.id,
'product_uom_qty': 10,
'product_uom': product.uom_id.id,
'price_unit': product.list_price})],
'pricelist_id': self.company_data['default_pricelist'].id,
}
so_company_A = sale_order.with_company(self.env.company).create(sale_order_vals2)
self.assertEqual(so_company_A.warehouse_id.id, self.warehouse_A.id)
@ -78,9 +76,103 @@ class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommo
'name': product.name,
'product_id': product.id,
'product_uom_qty': 10,
'product_uom': product.uom_id.id,
'price_unit': product.list_price})],
'pricelist_id': self.company_data['default_pricelist'].id,
}
so_company_B = sale_order.with_company(self.company_data_2['company']).create(sale_order_vals3)
self.assertEqual(so_company_B.warehouse_id.id, self.warehouse_B.id)
def test_sale_product_from_parent_company(self):
"""
Check that a product from a company can be sold by a branch
and that the resulting move can be created.
"""
parent_company = self.env.company
branch_company = self.env['res.company'].create({
'name': 'Branch Company',
'parent_id': parent_company.id,
})
self.product_a.company_id = parent_company
sale_order = self.env['sale.order'].with_company(branch_company).create({
'partner_id': self.partner_a.id,
'order_line': [(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_qty': 1,
})],
})
sale_order.action_confirm()
self.assertTrue(sale_order.picking_ids.move_ids)
def test_intercompany_transfer_sale_order_workflow(self):
company2 = self.company_data_2['company']
so = self.env['sale.order'].create({
'partner_id': company2.partner_id.id,
'order_line': [(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_qty': 5.0,
'product_uom_id': self.product_a.uom_id.id,
'price_unit': self.product_a.list_price})],
})
so.action_confirm()
picking = so.picking_ids
# create another move
self.env['stock.move'].create({
'picking_id': picking.id,
'location_id': picking.location_id.id,
'location_dest_id': picking.location_dest_id.id,
'product_id': self.product_b.id,
'product_uom_qty': 1,
'product_uom': self.product_b.uom_id.id,
'quantity': 1,
})
# ensure we have to moves in the picking
self.assertEqual(len(picking.move_ids), 2)
# make the moves as picked
picking.move_ids.picked = True
picking.button_validate()
# make sure an order line is created for the new stock move
self.assertEqual(len(picking.sale_id.order_line), 2)
def test_intercompany_show_lot_on_invoice(self):
"""
Check that lots and serial numbers are displayed on inter-companies invoices.
"""
self.env.user.group_ids |= self.env.ref('stock_account.group_lot_on_invoice')
company2 = self.company_data_2['company']
self.product_a.write({
'is_storable': 'True',
'tracking': 'serial',
'invoice_policy': 'delivery',
})
self.product_a.tracking = 'serial'
sn = self.env['stock.lot'].create({'name': 'SN0012', 'product_id': self.product_a.id})
self.env['stock.quant']._update_available_quantity(self.product_a, self.warehouse_A.lot_stock_id, 1.0, lot_id=sn)
so = self.env['sale.order'].create({
'partner_id': company2.partner_id.id,
'order_line': [Command.create({
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom_qty': 1.0,
'price_unit': self.product_a.list_price})],
})
so.action_confirm()
delivery = so.picking_ids
delivery.button_validate()
invoice = so._create_invoices()
invoice.action_post()
self.assertEqual(
[(rec['product_name'], rec['lot_id']) for rec in invoice._get_invoiced_lot_values()],
[(self.product_a.name, sn.id)]
)

View file

@ -5,8 +5,8 @@ from datetime import datetime, timedelta
from odoo.tools import html2plaintext
from odoo import Command
from odoo.tests import Form, tagged
from odoo.exceptions import AccessError
from odoo.tests.common import Form, tagged
from odoo.addons.stock.tests.test_report import TestReportsCommon
from odoo.addons.sale.tests.common import TestSaleCommon
@ -58,10 +58,10 @@ class TestSaleStockReports(TestReportsCommon):
line_2 = lines[1]
self.assertEqual(line_1['quantity'], 5)
self.assertTrue(line_1['replenishment_filled'])
self.assertEqual(line_1['document_out'].id, so_2.id)
self.assertEqual(line_1['document_out']['id'], so_2.id)
self.assertEqual(line_2['quantity'], 5)
self.assertEqual(line_2['replenishment_filled'], False)
self.assertEqual(line_2['document_out'].id, so_1.id)
self.assertEqual(line_2['document_out']['id'], so_1.id)
def test_report_forecast_2_report_line_corresponding_to_so_line_highlighted(self):
""" When accessing the report from a SO line, checks if the correct SO line is highlighted in the report
@ -82,7 +82,7 @@ class TestSaleStockReports(TestReportsCommon):
context = {"move_to_match_ids": so.order_line.move_ids.ids}
_, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids, context=context)
for line in lines:
if line['document_out'] == so:
if line['document_out']['id'] == so.id:
self.assertTrue(line['is_matched'], "The corresponding SO line should be matched in the forecast report.")
else:
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the SO shoud not be matched.")
@ -112,8 +112,8 @@ class TestSaleStockReports(TestReportsCommon):
_, _, lines = self.get_report_forecast(product_template_ids=product.product_tmpl_id.ids)
outgoing_line = next(filter(lambda line: line.get('document_out'), lines))
self.assertEqual(
(outgoing_line['document_out'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']),
(so, 3.0, True, True)
(outgoing_line['document_out']['id'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']['id']),
(so.id, 3.0, True, so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id).id)
)
stock_line = next(filter(lambda line: not line.get('document_out'), lines))
self.assertEqual(
@ -126,8 +126,8 @@ class TestSaleStockReports(TestReportsCommon):
_, _, lines = self.get_report_forecast(product_template_ids=product.product_tmpl_id.ids)
outgoing_line = next(filter(lambda line: line.get('document_out'), lines))
self.assertEqual(
(outgoing_line['document_out'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']),
(so, 3.0, True, False)
(outgoing_line['document_out']['id'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']),
(so.id, 3.0, True, False)
)
stock_line = next(filter(lambda line: not line.get('document_out'), lines))
self.assertEqual(
@ -163,7 +163,7 @@ class TestSaleStockReports(TestReportsCommon):
other = self.env['res.users'].create({
'name': 'Other Salesman',
'login': 'other',
'groups_id': [
'group_ids': [
Command.link(self.env.ref('sales_team.group_sale_salesman').id),
Command.link(self.env.ref('stock.group_stock_user').id),
],
@ -171,16 +171,52 @@ class TestSaleStockReports(TestReportsCommon):
# Need to reset the cache otherwise it wouldn't trigger an Access Error anyway as the Sale Order is already there.
sale_order.env.invalidate_all()
report_values = self.env['report.stock.report_product_product_replenishment'].with_user(other).get_report_values(docids=self.product.ids)
report_values = self.env['stock.forecasted_product_product'].with_user(other).get_report_values(docids=self.product.ids)
self.assertEqual(len(report_values['docs']['lines']), 1)
self.assertEqual(report_values['docs']['lines'][0]['document_out'], sale_order)
self.assertEqual(report_values['docs']['draft_sale_orders'], draft)
self.assertEqual(report_values['docs']['lines'][0]['document_out']['name'], sale_order.name)
self.assertEqual(len(report_values['docs']['product'][self.product.id]['draft_sale_orders']), 1)
self.assertEqual(report_values['docs']['product'][self.product.id]['draft_sale_orders'][0]['name'], draft.name)
# While 'other' can see these SO on the report, they shouldn't be able to access them.
with self.assertRaises(AccessError):
report_values['docs']['draft_sale_orders'].with_user(other).check_access_rule('read')
sale_order.with_user(other).check_access('read')
with self.assertRaises(AccessError):
report_values['docs']['lines'][0]['document_out'].with_user(other).check_access_rule('read')
draft.with_user(other).check_access('read')
def test_add_reference_remove_reference_works_with_multiple_records(self):
so = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [Command.create({
'product_id': self.product.id,
'product_uom_qty': 5,
})],
})
so.action_confirm()
so_delivery = so.picking_ids
so_delivery.reference_ids.copy()
picking_receipt = self.env['stock.picking'].create({
'picking_type_id': self.env.ref('stock.picking_type_in').id,
'partner_id': self.partner.id,
'move_ids': [Command.create({
'product_id': self.product.id,
'product_uom_qty': 18,
})],
})
picking_receipt.action_confirm()
self.env['report.stock.report_reception']._action_assign(
picking_receipt.move_ids,
so_delivery.move_ids,
)
self.assertEqual(picking_receipt.move_ids.reference_ids, so_delivery.move_ids.reference_ids)
self.env['report.stock.report_reception']._action_unassign(
picking_receipt.move_ids,
so_delivery.move_ids,
)
self.assertNotIn(picking_receipt.move_ids.reference_ids, so_delivery.move_ids.reference_ids)
@tagged('post_install', '-at_install')
@ -191,12 +227,12 @@ class TestSaleStockInvoices(TestSaleCommon):
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('stock.group_production_lot').id)]})
self.product_by_lot = self.env['product.product'].create({
'name': 'Product By Lot',
'type': 'product',
'is_storable': True,
'tracking': 'lot',
})
self.product_by_usn = self.env['product.product'].create({
'name': 'Product By USN',
'type': 'product',
'is_storable': True,
'tracking': 'serial',
})
self.warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
@ -204,17 +240,14 @@ class TestSaleStockInvoices(TestSaleCommon):
lot = self.env['stock.lot'].create({
'name': 'LOT0001',
'product_id': self.product_by_lot.id,
'company_id': self.env.company.id,
})
self.usn01 = self.env['stock.lot'].create({
'name': 'USN0001',
'product_id': self.product_by_usn.id,
'company_id': self.env.company.id,
})
self.usn02 = self.env['stock.lot'].create({
'name': 'USN0002',
'product_id': self.product_by_usn.id,
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 10, lot_id=lot)
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=self.usn01)
@ -228,7 +261,7 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
@ -239,7 +272,7 @@ class TestSaleStockInvoices(TestSaleCommon):
so.action_confirm()
picking = so.picking_ids
picking.move_ids.quantity_done = 5
picking.move_ids.write({'quantity': 5, 'picked': True})
picking.button_validate()
invoice = so._create_invoices()
@ -262,7 +295,7 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
self.product_by_lot.invoice_policy = "order"
@ -278,7 +311,7 @@ class TestSaleStockInvoices(TestSaleCommon):
invoice.action_post()
picking = so.picking_ids
picking.move_ids.quantity_done = 4
picking.move_ids.write({'quantity': 4, 'picked': True})
picking.button_validate()
html = self.env['ir.actions.report']._render_qweb_html(
@ -286,6 +319,40 @@ class TestSaleStockInvoices(TestSaleCommon):
text = html2plaintext(html)
self.assertRegex(text, r'Product By Lot\n4.00Units\nLOT0001', "There should be a line that specifies 4 x LOT0001")
def test_picking_description(self):
"""
Verify that for a no-variant product, the product name is not included as the first element in the picking description,
as this avoids repeating the name on the delivery slip.
"""
product_attr = self.env['product.attribute'].create({'name': 'Color', 'create_variant': 'no_variant'})
product_attrv1, product_attrv2 = self.env['product.attribute.value'].create([
{'name': 'Value1', 'attribute_id': product_attr.id},
{'name': 'Value2', 'attribute_id': product_attr.id},
])
product_template_no_variant = self.env['product.template'].create({
'name': 'product name',
'attribute_line_ids': [
Command.create({
'attribute_id': product_attr.id,
'value_ids': [Command.set([product_attrv1.id, product_attrv2.id])],
})]
})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({'name': product_template_no_variant.name,
'product_id': product_template_no_variant.product_variant_id.id,
'product_uom_qty': 4,
'product_no_variant_attribute_value_ids': product_template_no_variant.product_variant_id.attribute_line_ids.product_template_value_ids
}),
],
})
so.action_confirm()
picking = so.picking_ids[0]
picking_description = picking.move_ids._get_report_description_picking()
self.assertEqual(picking_description, 'Color: Value1\nColor: Value2')
def test_backorder_and_several_invoices(self):
"""
Suppose the lots are printed on the invoices.
@ -295,7 +362,7 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
@ -306,11 +373,8 @@ class TestSaleStockInvoices(TestSaleCommon):
so.action_confirm()
picking = so.picking_ids
picking.move_ids.move_line_ids[0].qty_done = 1
picking.move_ids.move_line_ids[0].quantity = 1
picking.button_validate()
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
invoice01 = so._create_invoices()
with Form(invoice01) as form:
@ -319,7 +383,7 @@ class TestSaleStockInvoices(TestSaleCommon):
invoice01.action_post()
backorder = picking.backorder_ids
backorder.move_ids.move_line_ids.qty_done = 1
backorder.move_ids.move_line_ids.quantity = 1
backorder.button_validate()
IrActionsReport = self.env['ir.actions.report']
@ -367,13 +431,12 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
lot01 = self.env['stock.lot'].search([('name', '=', 'LOT0001')])
lot02, lot03 = self.env['stock.lot'].create([{
'name': name,
'product_id': self.product_by_lot.id,
'company_id': self.env.company.id,
} for name in ['LOT0002', 'LOT0003']])
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 8, lot_id=lot02)
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 2, lot_id=lot03)
@ -388,39 +451,41 @@ class TestSaleStockInvoices(TestSaleCommon):
# Deliver 10 x LOT0001
delivery01 = so.picking_ids
delivery01.move_ids.quantity_done = 10
delivery01.move_ids.write({'quantity': 10, 'picked': True})
delivery01.button_validate()
self.assertEqual(delivery01.move_line_ids.lot_id.name, 'LOT0001')
# Return delivery01 (-> 10 x LOT0001)
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[delivery01.id], active_id=delivery01.id, active_model='stock.picking'))
return_wizard = return_form.save()
action = return_wizard.create_returns()
return_wizard.product_return_moves.quantity = 10
action = return_wizard.action_create_returns()
pick_return = self.env['stock.picking'].browse(action['res_id'])
move_form = Form(pick_return.move_ids, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
move_form = Form(pick_return.move_ids, view='stock.view_stock_move_operations')
with move_form.move_line_ids.edit(0) as line:
line.lot_id = lot01
line.qty_done = 10
line.quantity = 10
move_form.save()
pick_return.move_ids.picked = True
pick_return.button_validate()
# Return pick_return
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[pick_return.id], active_id=pick_return.id, active_model='stock.picking'))
return_wizard = return_form.save()
action = return_wizard.create_returns()
return_wizard.product_return_moves.quantity = 10
action = return_wizard.action_create_returns()
delivery02 = self.env['stock.picking'].browse(action['res_id'])
# Deliver 3 x LOT0002
delivery02.do_unreserve()
move_form = Form(delivery02.move_ids, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
move_form = Form(delivery02.move_ids, view='stock.view_stock_move_operations')
with move_form.move_line_ids.new() as line:
line.lot_id = lot02
line.qty_done = 3
line.quantity = 3
move_form.save()
action = delivery02.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
delivery02.move_ids.picked = True
Form.from_action(self.env, delivery02.button_validate()).save().process()
# Invoice 2 x P
invoice01 = so._create_invoices()
@ -438,14 +503,15 @@ class TestSaleStockInvoices(TestSaleCommon):
# Deliver 5 x LOT0002 + 2 x LOT0003
delivery03 = delivery02.backorder_ids
delivery03.do_unreserve()
move_form = Form(delivery03.move_ids, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
move_form = Form(delivery03.move_ids, view='stock.view_stock_move_operations')
with move_form.move_line_ids.new() as line:
line.lot_id = lot02
line.qty_done = 5
with move_form.move_line_nosuggest_ids.new() as line:
line.quantity = 5
with move_form.move_line_ids.new() as line:
line.lot_id = lot03
line.qty_done = 2
line.quantity = 2
move_form.save()
delivery03.move_ids.picked = True
delivery03.button_validate()
# Invoice 8 x P
@ -468,7 +534,7 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
@ -479,8 +545,9 @@ class TestSaleStockInvoices(TestSaleCommon):
so.action_confirm()
picking = so.picking_ids
picking.move_ids.move_line_ids[0].qty_done = 1
picking.move_ids.move_line_ids[1].qty_done = 1
picking.move_ids.move_line_ids[0].quantity = 1
picking.move_ids.move_line_ids[1].quantity = 1
picking.move_ids.picked = True
picking.button_validate()
invoice01 = so._create_invoices()
@ -493,26 +560,28 @@ class TestSaleStockInvoices(TestSaleCommon):
# Refund the invoice
refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
'refund_method': 'cancel',
'journal_id': invoice01.journal_id.id,
})
res = refund_wizard.reverse_moves()
res = refund_wizard.refund_moves()
refund_invoice = self.env['account.move'].browse(res['res_id'])
refund_invoice.action_post()
# recieve the returned product
stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(active_ids=picking.ids, active_id=picking.sorted().ids[0], active_model='stock.picking'))
return_wiz = stock_return_picking_form.save()
res = return_wiz.create_returns()
return_wiz.product_return_moves.quantity = 2
res = return_wiz.action_create_returns()
pick_return = self.env['stock.picking'].browse(res['res_id'])
move_form = Form(pick_return.move_ids, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
move_form = Form(pick_return.move_ids, view='stock.view_stock_move_operations')
with move_form.move_line_ids.edit(0) as line:
line.lot_id = self.usn01
line.qty_done = 1
with move_form.move_line_nosuggest_ids.new() as line:
line.quantity = 1
with move_form.move_line_ids.edit(1) as line:
line.lot_id = self.usn02
line.qty_done = 1
line.quantity = 1
move_form.save()
pick_return.move_ids.picked = True
pick_return.button_validate()
# reversed invoice
@ -530,7 +599,7 @@ class TestSaleStockInvoices(TestSaleCommon):
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
@ -541,7 +610,8 @@ class TestSaleStockInvoices(TestSaleCommon):
so.action_confirm()
picking = so.picking_ids
picking.move_ids.move_line_ids[0].qty_done = 1
picking.move_ids.move_line_ids[0].quantity = 1
picking.move_ids.picked = True
picking.button_validate()
invoice01 = so._create_invoices()
@ -553,10 +623,9 @@ class TestSaleStockInvoices(TestSaleCommon):
# Refund the invoice with full refund and new draft invoice
refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
'refund_method': 'modify',
'journal_id': invoice01.journal_id.id,
})
res = refund_wizard.reverse_moves()
res = refund_wizard.modify_moves()
invoice02 = self.env['account.move'].browse(res['res_id'])
invoice02.action_post()