mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-28 03:52:01 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -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
|
|
@ -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))
|
||||
|
|
@ -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},
|
||||
])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
1855
odoo-bringout-oca-ocb-sale_stock/sale_stock/tests/test_sale_stock.py
Normal file
1855
odoo-bringout-oca-ocb-sale_stock/sale_stock/tests/test_sale_stock.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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'))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue