Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_anglo_saxon_valuation
from . import test_anglo_saxon_valuation_reconciliation
from . import test_anglosaxon_account
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_multicompany
from . import test_sale_stock_accrued_entries
from . import test_sale_stock_access_rights
from . import test_create_perf

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,250 @@
# -*- 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.tests import Form, tagged
class TestValuationReconciliationCommon(ValuationReconciliationTestCommon):
@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,
})
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)],
})],
})
sale_order.invoice_ids += rslt
return rslt
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)
sale_order = self._create_sale(test_product, '2108-01-01')
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')
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)
sale_order = self._create_sale(test_product, '2018-01-01')
invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
invoice.action_post()
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')
#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],
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()
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._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'])
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')
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)
sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
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._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')
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),
])
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
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
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,
'order_line': [
(0, 0, {
'name': product.name,
'product_id': product.id,
'product_uom_qty': 2,
'product_uom': product.uom_po_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.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,
'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,
'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],
})
so.invoice_ids += inv
inv.action_post()
# Create in_moves for P1/P2 such that the first move compensates the out_svls
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
'product_id': product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': stock_loc.id,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'product_uom_qty': quantity,
'price_unit': product.standard_price + 1,
'picking_type_id': in_type.id,
} for product, quantity in zip(
[product_1, product_2],
[2.0, 2.0]
)])
in_moves._action_confirm()
for move in in_moves:
move.quantity_done = move.product_uom_qty
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))

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
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):
def test_cogs_should_use_price_from_the_right_company(self):
"""
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
# 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
company_b_standard_price = 10.0
product.with_company(company_b_data['company']).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')
# Post the invoice from company A with company B
company_a_invoice.with_context(allowed_company_ids=companies_with_b_first.ids).action_post()
# check cost used for anglo_saxon_line is from company A
anglo_saxon_lines = company_a_invoice.line_ids.filtered(lambda l: l.display_type == 'cogs')
self.assertRecordValues(anglo_saxon_lines, [
{'debit': 0.0, 'credit': company_a_standard_price},
{'debit': company_a_standard_price, 'credit': 0.0},
])

View file

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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
_logger = logging.getLogger(__name__)
@tagged('so_batch_perf')
class TestPERF(TransactionCaseWithUserDemo):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ENTITIES = 50
cls.products = cls.env['product.product'].create([{
'name': 'Product %s' % i,
'list_price': 1 + 10 * i,
'type': 'service',
} for i in range(10)])
cls.partners = cls.env['res.partner'].create([{
'name': 'Partner %s' % i,
} for i in range(cls.ENTITIES)])
cls.salesmans = cls.env.ref('base.user_admin') | cls.user_demo
cls.env.flush_all()
@users('admin')
@warmup
def test_empty_sale_order_creation_perf(self):
with self.assertQueryCount(admin=34):
self.env['sale.order'].create({
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
})
@users('admin')
@warmup
def test_empty_sales_orders_batch_creation_perf(self):
# + 1 SO insert
# + 1 SO sequence fetch
# + 1 warehouse fetch
# + 1 query to get analytic default account
# + 1 followers queries ?
with self.assertQueryCount(admin=39):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
} for i in range(2)])
@users('admin')
@warmup
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
with self.assertQueryCount(admin=44):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"}),
(0, 0, {"display_type": "line_section", "name": "SECTION"})
]
} for i in range(2)])
@users('admin')
@warmup
def test_light_sales_orders_batch_creation_perf_without_taxes(self):
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):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"}),
(0, 0, {"display_type": "line_section", "name": "SECTION"}),
(0, 0, {'product_id': self.products[0].id})
]
} for i in range(2)])
# Following tests are not deterministic
# And are privatized on purpose
# until the problem is found
@users('admin')
@warmup
def __test_light_sales_orders_batch_creation_perf(self):
with self.assertQueryCount(admin=70): # 69 locally, 70 in nightly runbot
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"}),
(0, 0, {"display_type": "line_section", "name": "SECTION"}),
(0, 0, {'product_id': self.products[0].id})
]
} for i in range(2)])
@users('admin')
@warmup
def __test_complex_sales_orders_batch_creation_perf(self):
# NOTE: sometimes more queries on runbot,
# do not change without verifying in multi-builds
# (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"
vals_list = [{
"partner_id": self.partners[i].id,
"user_id": self.salesmans[i % 2].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"})
] + [
(0, 0, {'product_id': product.id}) for product in self.products
],
} for i in range(self.ENTITIES)]
with self.assertQueryCount(admin=query_count):
t0 = time.time()
self.env["sale.order"].create(vals_list)
t1 = time.time()
_logger.info(MSG, 'sale.order', self.ENTITIES, "BATCH", t1 - t0)
self.env.cr.flush()
_logger.info(MSG, 'sale.order', self.ENTITIES, "FLUSH", time.time() - t1)
@users('admin')
@warmup
def __test_randomized_solines_qties(self):
"""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,
"user_id": self.salesmans[i % 2].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"})
] + [
(0, 0, {
'product_id': product.id,
'product_uom_qty': random.random()
}) for product in self.products
],
} for i in range(self.ENTITIES)]
# 1592 locally, 1593 in nightly runbot, 1954 sometimes
with self.assertQueryCount(admin=1593):
self.env["sale.order"].create(vals_list)

View file

@ -0,0 +1,115 @@
# -*- 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 odoo import fields
from odoo.tests import common, tagged
@tagged('post_install', '-at_install')
class TestSaleExpectedDate(ValuationReconciliationTestCommon):
def test_sale_order_expected_date(self):
""" Test expected date and effective date of Sales Orders """
Product = self.env['product.product']
product_A = Product.create({
'name': 'Product A',
'type': 'product',
'sale_delay': 5,
'uom_id': 1,
})
product_B = Product.create({
'name': 'Product B',
'type': 'product',
'sale_delay': 10,
'uom_id': 1,
})
product_C = Product.create({
'name': 'Product C',
'type': 'product',
'sale_delay': 15,
'uom_id': 1,
})
self.env['stock.quant']._update_available_quantity(product_A, self.company_data['default_warehouse'].lot_stock_id, 10)
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({
'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})
],
})
# if Shipping Policy is set to `direct`(when SO is in draft state) then expected date should be
# current date + shortest lead time from all of it's order lines
expected_date = fields.Datetime.now() + timedelta(days=5)
self.assertAlmostEqual(expected_date, sale_order.expected_date,
msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
# if Shipping Policy is set to `one`(when SO is in draft state) then expected date should be
# current date + longest lead time from all of it's order lines
sale_order.write({'picking_policy': 'one'})
expected_date = fields.Datetime.now() + timedelta(days=15)
self.assertAlmostEqual(expected_date, sale_order.expected_date,
msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
sale_order.action_confirm()
# Setting confirmation date of SO to 5 days from today so that the expected/effective date could be checked
# against real confirmation date
confirm_date = fields.Datetime.now() + timedelta(days=5)
sale_order.write({'date_order': confirm_date})
# if Shipping Policy is set to `one`(when SO is confirmed) then expected date should be
# SO confirmation date + longest lead time from all of it's order lines
expected_date = confirm_date + timedelta(days=15)
self.assertAlmostEqual(expected_date, sale_order.expected_date,
msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
# if Shipping Policy is set to `direct`(when SO is confirmed) then expected date should be
# SO confirmation date + shortest lead time from all of it's order lines
sale_order.write({'picking_policy': 'direct'})
expected_date = confirm_date + timedelta(days=5)
self.assertAlmostEqual(expected_date, sale_order.expected_date,
msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
# 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._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!")
def test_sale_order_commitment_date(self):
# 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({
'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,
})],
'commitment_date': '2010-07-12',
})
# I confirm the Sales Order.
new_order.action_confirm()
# I verify that the Procurements and Stock Moves have been generated with the correct date
security_delay = timedelta(days=new_order.company_id.security_lead)
commitment_date = fields.Datetime.from_string(new_order.commitment_date)
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")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged, Form
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.addons.mail.tests.common import mail_new_test_user
@tagged('post_install', '-at_install')
class TestControllersAccessRights(HttpCase, TestSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.portal_user = mail_new_test_user(cls.env, login='jimmy-portal', groups='base.group_portal')
def test_SO_and_DO_portal_acess(self):
""" Ensure that it is possible to open both SO and DO, either using the access token
or being connected as portal user"""
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.portal_user.partner_id
with so_form.order_line.new() as line:
line.product_id = self.product_a
so = so_form.save()
so.action_confirm()
picking = so.picking_ids
# Try to open SO/DO using the access token or being connected as portal user
for login in (None, self.portal_user.login):
so_url = '/my/orders/%s' % so.id
picking_url = '/my/picking/pdf/%s' % picking.id
self.authenticate(login, login)
if not login:
so._portal_ensure_token()
so_token = so.access_token
so_url = '%s?access_token=%s' % (so_url, so_token)
picking_url = '%s?access_token=%s' % (picking_url, so_token)
response = self.url_open(
url=so_url,
allow_redirects=False,
)
self.assertEqual(response.status_code, 200, 'Should be correct %s' % ('with a connected user' if login else 'using access token'))
response = self.url_open(
url=picking_url,
allow_redirects=False,
)
self.assertEqual(response.status_code, 200, 'Should be correct %s' % ('with a connected user' if login else 'using access token'))

View file

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
from odoo import fields, Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
from odoo.exceptions import UserError
@tagged('post_install', '-at_install')
class TestAccruedStockSaleOrders(AccountTestInvoicingCommon):
@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({
'name': "Product",
'list_price': 30.0,
'type': 'consu',
'uom_id': uom_unit.id,
'uom_po_id': 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_uom_qty': 10.0,
'product_uom': cls.product_order.uom_id.id,
'price_unit': cls.product_order.list_price,
'tax_id': False,
})
]
})
cls.sale_order.action_confirm()
cls.account_expense = cls.company_data['default_account_expense']
cls.account_revenue = cls.company_data['default_account_revenue']
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.button_validate()
wiz_act = pick.button_validate()
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
wiz.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({'date': fields.Date.to_date('2020-01-06')})
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': self.sale_order.ids,
}).create({
'account_id': self.account_expense.id,
'date': '2020-01-01',
})
# nothing to invoice on 2020-01-01
with self.assertRaises(UserError):
wizard.create_entries()
# 2 to invoice on 2020-01-04
wizard.date = fields.Date.to_date('2020-01-04')
self.assertRecordValues(self.env['account.move'].search(wizard.create_entries()['domain']).line_ids, [
# reverse move lines
{'account_id': self.account_revenue.id, 'debit': 60, 'credit': 0},
{'account_id': wizard.account_id.id, 'debit': 0, 'credit': 60},
# move lines
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 60},
{'account_id': wizard.account_id.id, 'debit': 60, 'credit': 0},
])
# 5 to invoice on 2020-01-07
wizard.date = fields.Date.to_date('2020-01-07')
self.assertRecordValues(self.env['account.move'].search(wizard.create_entries()['domain']).line_ids, [
# reverse move lines
{'account_id': self.account_revenue.id, 'debit': 150, 'credit': 0},
{'account_id': wizard.account_id.id, 'debit': 0, 'credit': 150},
# move lines
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 150},
{'account_id': wizard.account_id.id, 'debit': 150, 'credit': 0},
])
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.button_validate()
wiz_act = pick.button_validate()
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
wiz.process()
pick.move_ids.write({'date': fields.Date.to_date('2020-01-02')})
# invoice on 2020-01-04
inv = self.sale_order._create_invoices()
inv.invoice_date = fields.Date.to_date('2020-01-04')
inv.action_post()
# 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({'date': fields.Date.to_date('2020-01-06')})
# invoice on 2020-01-08
inv = self.sale_order._create_invoices()
inv.invoice_date = fields.Date.to_date('2020-01-08')
inv.action_post()
wizard = self.env['account.accrued.orders.wizard'].with_context({
'active_model': 'sale.order',
'active_ids': self.sale_order.ids,
}).create({
'account_id': self.company_data['default_account_expense'].id,
'date': '2020-01-02',
})
# 2 to invoice on 2020-01-07
self.assertRecordValues(self.env['account.move'].search(wizard.create_entries()['domain']).line_ids, [
# reverse move lines
{'account_id': self.account_revenue.id, 'debit': 60, 'credit': 0},
{'account_id': wizard.account_id.id, 'debit': 0, 'credit': 60},
# move lines
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 60},
{'account_id': wizard.account_id.id, 'debit': 60, 'credit': 0},
])
# nothing to invoice on 2020-01-05
wizard.date = fields.Date.to_date('2020-01-05')
with self.assertRaises(UserError):
wizard.create_entries()
# 3 to invoice on 2020-01-07
wizard.date = fields.Date.to_date('2020-01-07')
self.assertRecordValues(self.env['account.move'].search(wizard.create_entries()['domain']).line_ids, [
# reverse move lines
{'account_id': self.account_revenue.id, 'debit': 90, 'credit': 0},
{'account_id': wizard.account_id.id, 'debit': 0, 'credit': 90},
# move lines
{'account_id': self.account_revenue.id, 'debit': 0, 'credit': 90},
{'account_id': wizard.account_id.id, 'debit': 90, 'credit': 0},
])
# nothing to invoice on 2020-01-09
wizard.date = fields.Date.to_date('2020-01-09')
with self.assertRaises(UserError):
wizard.create_entries()

View file

@ -0,0 +1,240 @@
# -*- 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 import fields
from odoo.tests import tagged
from datetime import timedelta
@tagged('post_install', '-at_install')
class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# Update the product_1 with type and Customer Lead Time
cls.test_product_order.sale_delay = 5.0
def test_00_product_company_level_delays(self):
""" In order to check schedule date, set product's Customer Lead Time
and company's Sales Safety Days."""
# Update company with Sales Safety Days
self.env.company.security_lead = 3.00
# 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,
})]
})
self.assertEqual(order.order_line.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, "Picking should be created.")
# Check schedule date of picking
out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
min_date = order.picking_ids[0].scheduled_date
self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.')
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'})
# Set delay on pull rule
for pull_rule in self.company_data['default_warehouse'].delivery_route_id.rule_ids:
pull_rule.write({'delay': 2})
# Create sale order of product_1
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,
'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 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_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_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_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.')
def test_02_delivery_date_propagation(self):
""" In order to check deadline date propagation, set product's Customer Lead Time
and warehouse route's delay in stock rules"""
# Example :
# -> Set Warehouse with Outgoing Shipments : pick + pack + ship
# -> Set Delay : 5 days on stock rules
# -> Set Customer Lead Time on product : 30 days
# -> Set Sales Safety Days : 2 days
# -> Create an SO and confirm it with confirmation Date : 12/18/2018
# -> Pickings : OUT -> Scheduled Date : 01/12/2019, Deadline Date: 01/14/2019
# PACK -> Scheduled Date : 01/07/2019, Deadline Date: 01/09/2019
# PICK -> Scheduled Date : 01/02/2019, Deadline Date: 01/04/2019
# -> Now, change commitment_date in the sale order = out_deadline_date + 5 days
# -> Deadline Date should be changed and Scheduled date should be unchanged:
# OUT -> Deadline Date : 01/19/2019
# PACK -> Deadline Date : 01/14/2019
# PICK -> Deadline Date : 01/09/2019
# 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'})
# Set delay on pull rule
self.company_data['default_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})
# 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,
'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 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)
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),
msg='Deadline date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.')
out_scheduled_date = deadline_date - timedelta(days=self.env.company.security_lead)
self.assertAlmostEqual(
out.scheduled_date, out_scheduled_date, delta=timedelta(seconds=1),
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_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),
msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
deadline_date -= timedelta(days=pack.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
pack.date_deadline, deadline_date, delta=timedelta(seconds=1),
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_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),
msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
deadline_date -= timedelta(days=pick.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
pick.date_deadline, deadline_date, delta=timedelta(seconds=1),
msg='Deadline date of pack type picking should be equal to: Deadline date of ship type picking - pull rule delay.')
# Now change the commitment date (Delivery Date) of the sale order
new_deadline = deadline_date + timedelta(days=5)
order.write({'commitment_date': new_deadline})
# Now check date_deadline of pick, pack and out are forced
# TODO : add note in case of change of deadline and check
self.assertEqual(out.date_deadline, new_deadline)
new_deadline -= timedelta(days=pack.move_ids[0].rule_id.delay)
self.assertEqual(pack.date_deadline, new_deadline)
new_deadline -= timedelta(days=pick.move_ids[0].rule_id.delay)
self.assertEqual(pick.date_deadline, new_deadline)
# Removes the SO deadline and checks the delivery deadline is updated accordingly.
order.commitment_date = False
new_deadline = order.expected_date
self.assertEqual(out.date_deadline, new_deadline)
new_deadline -= timedelta(days=pack.move_ids.rule_id.delay)
self.assertEqual(pack.date_deadline, new_deadline)
new_deadline -= timedelta(days=pick.move_ids.rule_id.delay)
self.assertEqual(pick.date_deadline, new_deadline)
def test_03_product_company_level_delays(self):
"""Partial duplicate of test_02 to make sure there is no default value specified in sale
that disables the computation of the customer_lead.
"""
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 = 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,
'order_id': order.id,
})
self.assertEqual(order_line.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, "Picking should be created.")
# Check schedule date of picking
out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
min_date = order.picking_ids[0].scheduled_date
self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.')

View file

@ -0,0 +1,86 @@
# -*- 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.tests import tagged
@tagged('post_install', '-at_install')
class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.warehouse_A = cls.company_data['default_warehouse']
cls.warehouse_A2 = 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.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.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
def test_warehouse_definition_on_so(self):
partner = self.partner_a
product = self.test_product_order
sale_order_vals = {
'partner_id': partner.id,
'partner_invoice_id': partner.id,
'partner_shipping_id': partner.id,
'user_id': False,
'company_id': self.env.company.id,
'order_line': [(0, 0, {
'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']
so_no_user = sale_order.create(sale_order_vals)
self.assertFalse(so_no_user.user_id.property_warehouse_id)
self.assertEqual(so_no_user.warehouse_id.id, self.warehouse_A.id)
sale_order_vals2 = {
'partner_id': partner.id,
'partner_invoice_id': partner.id,
'partner_shipping_id': partner.id,
'company_id': self.env.company.id,
'order_line': [(0, 0, {
'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)
sale_order_vals3 = {
'partner_id': partner.id,
'partner_invoice_id': partner.id,
'partner_shipping_id': partner.id,
'company_id': self.company_data_2['company'].id,
'order_line': [(0, 0, {
'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)

View file

@ -0,0 +1,566 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo.tools import html2plaintext
from odoo import Command
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
class TestSaleStockReports(TestReportsCommon):
def test_report_forecast_1_sale_order_replenishment(self):
""" Create and confirm two sale orders: one for the next week and one
for tomorrow. Then check in the report it's the most urgent who is
linked to the qty. on stock.
"""
# make sure first picking doesn't auto-assign
self.picking_type_out.reservation_method = 'manual'
today = datetime.today()
# Put some quantity in stock.
quant_vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'location_id': self.stock_location.id,
'quantity': 5,
'reserved_quantity': 0,
}
self.env['stock.quant'].create(quant_vals)
# Create a first SO for the next week.
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner
# so_form.validity_date = today + timedelta(days=7)
with so_form.order_line.new() as so_line:
so_line.product_id = self.product
so_line.product_uom_qty = 5
so_1 = so_form.save()
so_1.action_confirm()
so_1.picking_ids.scheduled_date = today + timedelta(days=7)
# Create a second SO for tomorrow.
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner
# so_form.validity_date = today + timedelta(days=1)
with so_form.order_line.new() as so_line:
so_line.product_id = self.product
so_line.product_uom_qty = 5
so_2 = so_form.save()
so_2.action_confirm()
so_2.picking_ids.scheduled_date = today + timedelta(days=1)
report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
self.assertEqual(len(lines), 2)
line_1 = lines[0]
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_2['quantity'], 5)
self.assertEqual(line_2['replenishment_filled'], False)
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
"""
# We create 2 identical SO
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner
with so_form.order_line.new() as line:
line.product_id = self.product
line.product_uom_qty = 5
so1 = so_form.save()
so1.action_confirm()
so2 = so1.copy()
so2.action_confirm()
# Check for both SO if the highlight (is_matched) corresponds to the correct SO
for so in [so1, so2]:
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:
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.")
def test_report_forecast_3_unreserve_2_step_delivery(self):
"""
Check that the forecast correctly reconciles the outgoing moves
that are part of a chain with stock availability when unreserved.
"""
warehouse = self.env.ref("stock.warehouse0")
warehouse.delivery_steps = 'pick_ship'
product = self.product
# Put 5 units in stock
self.env['stock.quant']._update_available_quantity(product, warehouse.lot_stock_id, 5)
# Create and confirm an SO for 3 units
so = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [
Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 3,
}),
],
})
so.action_confirm()
_, _, 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)
)
stock_line = next(filter(lambda line: not line.get('document_out'), lines))
self.assertEqual(
(stock_line['quantity'], stock_line['replenishment_filled'], stock_line['reservation']),
(2.0, True, False)
)
# unrerseve the PICK delivery
pick_delivery = so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id)
pick_delivery.do_unreserve()
_, _, 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)
)
stock_line = next(filter(lambda line: not line.get('document_out'), lines))
self.assertEqual(
(stock_line['quantity'], stock_line['replenishment_filled'], stock_line['reservation']),
(2.0, True, False)
)
def test_report_forecast_4_so_from_another_salesman(self):
""" Try accessing the forecast with a user that has only access to his SO while another user has created:
- A draft Sale Order
- A confirmed Sale Order
The report shoud be usable by that user, and while he cannot open those SO, he should still see them to have the correct
informations in the report.
"""
# Create the SO & confirm it with first user
with Form(self.env['sale.order']) as so_form:
so_form.partner_id = self.partner
with so_form.order_line.new() as line:
line.product_id = self.product
line.product_uom_qty = 3
sale_order = so_form.save()
sale_order.action_confirm()
# Create a draft SO with the same user for the same product
with Form(self.env['sale.order']) as so_form:
so_form.partner_id = self.partner
with so_form.order_line.new() as line:
line.product_id = self.product
line.product_uom_qty = 2
draft = so_form.save()
# Create second user which only has access to its own documents
other = self.env['res.users'].create({
'name': 'Other Salesman',
'login': 'other',
'groups_id': [
Command.link(self.env.ref('sales_team.group_sale_salesman').id),
Command.link(self.env.ref('stock.group_stock_user').id),
],
})
# 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)
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)
# 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')
with self.assertRaises(AccessError):
report_values['docs']['lines'][0]['document_out'].with_user(other).check_access_rule('read')
@tagged('post_install', '-at_install')
class TestSaleStockInvoices(TestSaleCommon):
def setUp(self):
super(TestSaleStockInvoices, self).setUp()
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',
'tracking': 'lot',
})
self.product_by_usn = self.env['product.product'].create({
'name': 'Product By USN',
'type': 'product',
'tracking': 'serial',
})
self.warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
self.stock_location = self.warehouse.lot_stock_id
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)
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=self.usn02)
def test_invoice_less_than_delivered(self):
"""
Suppose the lots are printed on the invoices.
A user invoice a tracked product with a smaller quantity than delivered.
On the invoice, the quantity of the used lot should be the invoiced one.
"""
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)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 5}),
],
})
so.action_confirm()
picking = so.picking_ids
picking.move_ids.quantity_done = 5
picking.button_validate()
invoice = so._create_invoices()
with Form(invoice) as form:
with form.invoice_line_ids.edit(0) as line:
line.quantity = 2
invoice.action_post()
html = self.env['ir.actions.report']._render_qweb_html(
'account.report_invoice_with_payments', invoice.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0001', "There should be a line that specifies 2 x LOT0001")
def test_invoice_before_delivery(self):
"""
Suppose the lots are printed on the invoices.
The user sells a tracked product, its invoicing policy is "Ordered quantities"
A user invoice a tracked product with a smaller quantity than delivered.
On the invoice, the quantity of the used lot should be the invoiced one.
"""
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.product_by_lot.invoice_policy = "order"
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 4}),
],
})
so.action_confirm()
invoice = so._create_invoices()
invoice.action_post()
picking = so.picking_ids
picking.move_ids.quantity_done = 4
picking.button_validate()
html = self.env['ir.actions.report']._render_qweb_html(
'account.report_invoice_with_payments', invoice.ids)[0]
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_backorder_and_several_invoices(self):
"""
Suppose the lots are printed on the invoices.
The user sells 2 tracked-by-usn products, he delivers 1 product and invoices it
Then, he delivers the other one and invoices it too. Each invoice should have the
correct USN
"""
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)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
],
})
so.action_confirm()
picking = so.picking_ids
picking.move_ids.move_line_ids[0].qty_done = 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:
with form.invoice_line_ids.edit(0) as line:
line.quantity = 1
invoice01.action_post()
backorder = picking.backorder_ids
backorder.move_ids.move_line_ids.qty_done = 1
backorder.button_validate()
IrActionsReport = self.env['ir.actions.report']
html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
self.assertNotIn('USN0002', text)
invoice02 = so._create_invoices()
invoice02.action_post()
html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
self.assertNotIn('USN0001', text)
# Posting the second invoice shouldn't change the result of the first one
html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should still be a line that specifies 1 x USN0001")
self.assertNotIn('USN0002', text)
# Resetting and posting again the first invoice shouldn't change the results
invoice01.button_draft()
invoice01.action_post()
html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should still be a line that specifies 1 x USN0001")
self.assertNotIn('USN0002', text)
html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
self.assertNotIn('USN0001', text)
def test_invoice_with_several_returns(self):
"""
Mix of returns and partial invoice
- Product P tracked by lot
- SO with 10 x P
- Deliver 10 x Lot01
- Return 10 x Lot01
- Deliver 03 x Lot02
- Invoice 02 x P
- Deliver 05 x Lot02 + 02 x Lot03
- Invoice 08 x P
"""
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)]})
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)
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 10}),
],
})
so.action_confirm()
# Deliver 10 x LOT0001
delivery01 = so.picking_ids
delivery01.move_ids.quantity_done = 10
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()
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:
line.lot_id = lot01
line.qty_done = 10
move_form.save()
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()
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:
line.lot_id = lot02
line.qty_done = 3
move_form.save()
action = delivery02.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
# Invoice 2 x P
invoice01 = so._create_invoices()
with Form(invoice01) as form:
with form.invoice_line_ids.edit(0) as line:
line.quantity = 2
invoice01.action_post()
html = self.env['ir.actions.report']._render_qweb_html(
'account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0002', "There should be a line that specifies 2 x LOT0002")
self.assertNotIn('LOT0001', text)
# 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:
line.lot_id = lot02
line.qty_done = 5
with move_form.move_line_nosuggest_ids.new() as line:
line.lot_id = lot03
line.qty_done = 2
move_form.save()
delivery03.button_validate()
# Invoice 8 x P
invoice02 = so._create_invoices()
invoice02.action_post()
html = self.env['ir.actions.report']._render_qweb_html(
'account.report_invoice_with_payments', invoice02.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By Lot\n6.00Units\nLOT0002', "There should be a line that specifies 6 x LOT0002")
self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0003', "There should be a line that specifies 2 x LOT0003")
self.assertNotIn('LOT0001', text)
def test_refund_cancel_invoices(self):
"""
Suppose the lots are printed on the invoices.
The user sells 2 tracked-by-usn products, he delivers 2 products and invoices them
Then he adds credit notes and issues a full refund. Receive the products.
The reversed invoice should also have correct USN
"""
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)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
],
})
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.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
# 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()
refund_invoice = self.env['account.move'].browse(res['res_id'])
# 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()
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:
line.lot_id = self.usn01
line.qty_done = 1
with move_form.move_line_nosuggest_ids.new() as line:
line.lot_id = self.usn02
line.qty_done = 1
move_form.save()
pick_return.button_validate()
# reversed invoice
html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', refund_invoice.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
def test_refund_modify_invoices(self):
"""
Suppose the lots are printed on the invoices.
The user sells 1 tracked-by-usn products, he delivers 1 and invoices it
Then he adds credit notes and issues full refund and new draft invoice.
The new draft invoice should have correct USN
"""
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)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 1}),
],
})
so.action_confirm()
picking = so.picking_ids
picking.move_ids.move_line_ids[0].qty_done = 1
picking.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
# 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()
invoice02 = self.env['account.move'].browse(res['res_id'])
invoice02.action_post()
# new draft invoice
html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")