[REF] stock: move stock.inventory feature into stock.quant

This commit removes stock.inventory(.line) and moves its general
functionality into stock.quant. Some features are lost during this
switch as well.

Feature Additions:
- New single list view for inventory adjustments (no more multiple
  inventory adjustment records to keep track of!)
- Improved cyclic counts (annual inventory day setting) + next inventory
  dates are immediately viewable in view (vs auto-generated inventories
  based only on location)
- Specific quants (i.e. counts) can be assigned to users for more
  flexibility (vs only able to restrict by location + product
  combinations)
- Counts can be requested (i.e. bulk assigned to user/for a specific
  inventory date)

Feature Removals:
- Can no longer look at previous inventory adjustments linked to a
  specific record. Each quant has a history button to show inventory
  related moves. [relevant account moves are also now harder to see via
  stock as well]

Other changes:
- Bulk of changes were for demo/test
- Some changes were done to ensure "Update Quantity"/"Inventory Report"
  views still mainly function the same as before with the exception of a
  new column added to support updating quant quantities in these views.

Task: 2440026
ENT PR: odoo/enterprise#17329
Upgrade PR: odoo/upgrade#2326

closes odoo/odoo#68409

Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
Arnold Moyaux
2021-04-02 11:51:38 +00:00
parent c483f59b37
commit bdcb3d192b
73 changed files with 1411 additions and 2876 deletions
+27 -39
View File
@@ -509,67 +509,55 @@
<!-- Initital Inventory -->
<record id="stock_inventory_drawer" model="stock.inventory">
<field name="name">Inventory: Drawer</field>
</record>
<record id="stock_inventory_drawer_lot0" model="stock.inventory.line">
<record id="stock_inventory_drawer_lot0" model="stock.quant">
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">50</field>
<field name="prod_lot_id" ref="lot_product_27_0"/>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="lot_id" ref="lot_product_27_0"/>
</record>
<record id="stock_inventory_drawer_lot1" model="stock.inventory.line">
<record id="stock_inventory_drawer_lot1" model="stock.quant">
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">40</field>
<field name="prod_lot_id" ref="lot_product_27_1"/>
<field name="inventory_quantity">40.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="lot_id" ref="lot_product_27_1"/>
</record>
<record id="stock_inventory_line_product_drawer_drawer" model="stock.inventory.line">
<record id="stock_inventory_product_drawer_drawer" model="stock.quant">
<field name="product_id" ref="product_product_drawer_drawer"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">50</field>
<field name="prod_lot_id" ref="lot_product_product_drawer_drawer_0"/>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="lot_id" ref="lot_product_product_drawer_drawer_0"/>
</record>
<record id="stock_inventory_line_product_drawer_case" model="stock.inventory.line">
<record id="stock_inventory_product_drawer_case" model="stock.quant">
<field name="product_id" ref="product_product_drawer_case"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">50</field>
<field name="prod_lot_id" ref="lot_product_product_drawer_case_0"/>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="lot_id" ref="lot_product_product_drawer_case_0"/>
</record>
<record id="stock_inventory_line_product_wood_panel" model="stock.inventory.line">
<record id="stock_inventory_product_wood_panel" model="stock.quant">
<field name="product_id" ref="product_product_wood_panel"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">50</field>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_product_ply" model="stock.inventory.line">
<record id="stock_inventory_product_ply" model="stock.quant">
<field name="product_id" ref="product_product_wood_ply"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">20</field>
<field name="inventory_quantity">20.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_product_wear" model="stock.inventory.line">
<record id="stock_inventory_product_wear" model="stock.quant">
<field name="product_id" ref="product_product_wood_wear"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_drawer"/>
<field name="product_qty">30</field>
<field name="inventory_quantity">30.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<function model="stock.inventory" name="_action_done">
<function eval="[[('state','=','draft'), ('id', '=', ref('stock_inventory_drawer'))]]" model="stock.inventory" name="search"/>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_drawer_lot0'),
ref('stock_inventory_drawer_lot1'),
ref('stock_inventory_product_drawer_drawer'),
ref('stock_inventory_product_drawer_case'),
ref('stock_inventory_product_wood_panel'),
ref('stock_inventory_product_ply'),
ref('stock_inventory_product_wear'),
))]]" model="stock.quant" name="search"/>
</function>
<!-- BoM -->
+1 -1
View File
@@ -55,7 +55,7 @@ class TestMrpProductionBackorder(TestMrpCommon):
'product_id': product.id,
'inventory_quantity': 100,
'location_id': production.location_src_id.id,
})
})._apply_inventory()
production.action_assign()
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, False)
+13 -19
View File
@@ -43,22 +43,16 @@ class TestMrpOrder(TestMrpCommon):
consume strictly what's needed. """
self.product_1.type = 'product'
self.product_2.type = 'product'
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
'line_ids': [(0, 0, {
'product_id': self.product_1.id,
'product_uom_id': self.product_1.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
}), (0, 0, {
'product_id': self.product_2.id,
'product_uom_id': self.product_2.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
})]
})
inventory.action_start()
inventory.action_validate()
self.env['stock.quant'].create({
'location_id': self.warehouse_1.lot_stock_id.id,
'product_id': self.product_1.id,
'inventory_quantity': 500
}).action_apply_inventory()
self.env['stock.quant'].create({
'location_id': self.warehouse_1.lot_stock_id.id,
'product_id': self.product_2.id,
'inventory_quantity': 500
}).action_apply_inventory()
test_date_planned = Dt.now() - timedelta(days=1)
test_quantity = 2.0
@@ -107,7 +101,7 @@ class TestMrpOrder(TestMrpCommon):
backorder.save().action_close_mo()
self.assertEqual(man_order.state, 'done', "Production order should be done.")
def test_production_avialability(self):
def test_production_availability(self):
""" Checks the availability of a production order through mutliple calls to `action_assign`.
"""
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
@@ -132,7 +126,7 @@ class TestMrpOrder(TestMrpCommon):
'product_id': self.product_2.id,
'inventory_quantity': 2.0,
'location_id': self.stock_location_14.id
})
}).action_apply_inventory()
production_2.action_assign()
# check sub product availability state is partially available
@@ -143,7 +137,7 @@ class TestMrpOrder(TestMrpCommon):
'product_id': self.product_2.id,
'inventory_quantity': 5.0,
'location_id': self.stock_location_14.id
})
}).action_apply_inventory()
production_2.action_assign()
# check sub product availability state is assigned
+2 -2
View File
@@ -60,7 +60,7 @@ class TestProcurement(TestMrpCommon):
'product_id': self.product_2.id,
'inventory_quantity': 48,
'location_id': self.warehouse.lot_stock_id.id,
})
}).action_apply_inventory()
produce_product_4.action_assign()
self.assertEqual(produce_product_4.product_qty, 8, "Wrong quantity of finish product.")
self.assertEqual(produce_product_4.product_uom_id, self.uom_dozen, "Wrong quantity of finish product.")
@@ -84,7 +84,7 @@ class TestProcurement(TestMrpCommon):
'product_id': self.product_2.id,
'inventory_quantity': 12,
'location_id': self.warehouse.lot_stock_id.id,
})
}).action_apply_inventory()
production_product_6.action_assign()
# ------------------------------------
+19 -26
View File
@@ -26,17 +26,11 @@ class TestWarehouse(common.TestMrpCommon):
'name': 'Assembly Line 1',
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
})
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
'line_ids': [(0, 0, {
'product_id': self.graphics_card.id,
'product_uom_id': self.graphics_card.uom_id.id,
'product_qty': 16.0,
'location_id': self.stock_location_14.id,
})]
})
inventory.action_start()
inventory.action_validate()
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'product_id': self.graphics_card.id,
'inventory_quantity': 16.0
}).action_apply_inventory()
self.bom_laptop = self.env['mrp.bom'].create({
'product_tmpl_id': self.laptop.product_tmpl_id.id,
@@ -112,22 +106,21 @@ class TestWarehouse(common.TestMrpCommon):
'company_id': self.env.company.id,
})
stock_inv_product_4 = self.env['stock.inventory'].create({
'name': 'Stock Inventory for Stick',
'product_ids': [(4, self.product_4.id)],
'line_ids': [
(0, 0, {'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 8, 'prod_lot_id': lot_product_4.id, 'location_id': self.stock_location_14.id}),
]})
# Inventory for Stick
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'product_id': self.product_4.id,
'inventory_quantity': 8,
'lot_id': lot_product_4.id
}).action_apply_inventory()
stock_inv_product_2 = self.env['stock.inventory'].create({
'name': 'Stock Inventory for Stone Tools',
'product_ids': [(4, self.product_2.id)],
'line_ids': [
(0, 0, {'product_id': self.product_2.id, 'product_uom_id': self.product_2.uom_id.id, 'product_qty': 12, 'prod_lot_id': lot_product_2.id, 'location_id': self.stock_location_14.id})
]})
(stock_inv_product_4 | stock_inv_product_2)._action_start()
stock_inv_product_2.action_validate()
stock_inv_product_4.action_validate()
# Inventory for Stone Tools
self.env['stock.quant'].create({
'location_id': self.stock_location_14.id,
'product_id': self.product_2.id,
'inventory_quantity': 12,
'lot_id': lot_product_2.id
}).action_apply_inventory()
#Create Manufacturing order.
production_form = Form(self.env['mrp.production'])
+33 -20
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form, tagged
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
import uuid
class TestTraceability(TestMrpCommon):
TRACKING_TYPES = ['none', 'serial', 'lot']
@@ -24,21 +24,39 @@ class TestTraceability(TestMrpCommon):
consumed_lot = self._create_product('lot')
consumed_serial = self._create_product('serial')
stock_id = self.env.ref('stock.stock_location_stock').id
inventory_adjustment = self.env['stock.inventory'].create({
'name': 'Initial Inventory',
'location_ids': [(4, stock_id)],
Lot = self.env['stock.production.lot']
# create inventory
quants = self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_no_track.id,
'inventory_quantity': 3
})
inventory_adjustment.action_start()
inventory_adjustment.write({
'line_ids': [
(0,0, {'product_id': consumed_no_track.id, 'product_qty': 3, 'location_id': stock_id}),
(0,0, {'product_id': consumed_lot.id, 'product_qty': 3, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
(0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
(0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
(0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
]
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_lot.id,
'inventory_quantity': 3,
'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id
})
inventory_adjustment.action_validate()
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants |= self.env['stock.quant'].create({
'location_id': stock_id,
'product_id': consumed_serial.id,
'inventory_quantity': 1,
'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id
})
quants.action_apply_inventory()
for finished_product in [finished_no_track, finished_lot, finished_serial]:
bom = self.env['mrp.bom'].create({
'product_id': finished_product.id,
@@ -78,9 +96,7 @@ class TestTraceability(TestMrpCommon):
ml.qty_done = 1
details_operation_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check results of traceability
@@ -89,9 +105,7 @@ class TestTraceability(TestMrpCommon):
'model': 'mrp.production',
})
lines = self.env['stock.traceability.report'].with_context(context).get_lines()
self.assertEqual(len(lines), 1, "Should always return 1 line : the final product")
final_product = lines[0]
self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable")
@@ -101,7 +115,6 @@ class TestTraceability(TestMrpCommon):
'model_id': final_product['model_id'],
'model_name': final_product['model'],
})
self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial")
for line in lines:
+22 -25
View File
@@ -143,32 +143,29 @@ class TestMrpAccount(TestMrpCommon):
self.product_screw.standard_price = 0.1
self.product_table_leg.tracking = 'none'
self.product_table_sheet.tracking = 'none'
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
'line_ids': [(0, 0, {
'product_id': self.product_table_sheet.id, # tracking serial
'product_uom_id': self.product_table_sheet.uom_id.id,
'product_qty': 20,
'location_id': self.source_location_id
}), (0, 0, {
'product_id': self.product_table_leg.id, # tracking lot
'product_uom_id': self.product_table_leg.uom_id.id,
'product_qty': 20,
'location_id': self.source_location_id
}), (0, 0, {
'product_id': self.product_bolt.id,
'product_uom_id': self.product_bolt.uom_id.id,
'product_qty': 20,
'location_id': self.source_location_id
}), (0, 0, {
'product_id': self.product_screw.id,
'product_uom_id': self.product_screw.uom_id.id,
'product_qty': 200000,
'location_id': self.source_location_id
}),
]
# Inventory Product Table
quants = self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_table_sheet.id, # tracking serial
'inventory_quantity': 20,
'location_id': self.source_location_id,
})
inventory.action_validate
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_table_leg.id, # tracking lot
'inventory_quantity': 20,
'location_id': self.source_location_id,
})
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_bolt.id,
'inventory_quantity': 20,
'location_id': self.source_location_id,
})
quants |= self.env['stock.quant'].create({
'product_id': self.product_screw.id,
'inventory_quantity': 200000,
'location_id': self.source_location_id,
})
quants.action_apply_inventory()
bom = self.mrp_bom_desk.copy()
bom.operation_ids = False
production_table_form = Form(self.env['mrp.production'])
@@ -80,22 +80,18 @@ class TestStockLandedCostsMrp(ValuationReconciliationTestCommon):
})
def test_landed_cost_on_mrp(self):
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
'line_ids': [(0, 0, {
'product_id': self.product_component1.id,
'product_uom_id': self.product_component1.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
}), (0, 0, {
'product_id': self.product_component2.id,
'product_uom_id': self.product_component2.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
})]
# Initial inventory
quants = self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_component1.id,
'inventory_quantity': 500,
'location_id': self.warehouse_1.lot_stock_id.id,
})
inventory.action_start()
inventory.action_validate()
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product_component2.id,
'inventory_quantity': 500,
'location_id': self.warehouse_1.lot_stock_id.id,
})
quants.action_apply_inventory()
man_order_form = Form(self.env['mrp.production'].with_user(self.allow_user))
man_order_form.product_id = self.product_refrigerator
@@ -120,8 +116,6 @@ class TestStockLandedCostsMrp(ValuationReconciliationTestCommon):
mo_form = Form(man_order.with_user(self.allow_user))
mo_form.qty_producing = 2
man_order = mo_form.save()
man_order.button_mark_done()
landed_cost = Form(self.env['stock.landed.cost'].with_user(self.allow_user)).save()
+3 -10
View File
@@ -474,19 +474,12 @@ class TestPoSCommon(ValuationReconciliationTestCommon):
def adjust_inventory(cls, products, quantities):
""" Adjust inventory of the given products
"""
inventory = cls.env['stock.inventory'].create({
'name': 'Inventory adjustment'
})
for product, qty in zip(products, quantities):
cls.env['stock.inventory.line'].create({
cls.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'inventory_id': inventory.id,
'product_qty': qty,
'inventory_quantity': qty,
'location_id': cls.stock_location_components.id,
})
inventory._action_start()
inventory.action_validate()
}).action_apply_inventory()
def open_new_session(self):
""" Used to open new pos session in each configuration.
@@ -136,13 +136,13 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon):
'product_id': self.product.id,
'inventory_quantity': 5.0,
'location_id': self.warehouse.lot_stock_id.id,
})
}).action_apply_inventory()
self.product.standard_price = 1.0
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product.id,
'inventory_quantity': 10.0,
'location_id': self.warehouse.lot_stock_id.id,
})
}).action_apply_inventory()
self.assertEqual(self.product.value_svl, 30, "Value should be (5*5 + 5*1) = 30")
self.assertEqual(self.product.quantity_svl, 10)
@@ -99,13 +99,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
'categ_id': self.env.ref('product.product_category_all').id,
})
inventory = self.env['stock.inventory'].create({
'name': 'add product2',
'location_ids': [(4, self.stock_location.id)],
'product_ids': [(4, self.product2.id)],
})
inventory.action_start()
lot1 = self.env['stock.production.lot'].create({
'name': '1001',
'product_id': self.product2.id,
@@ -117,24 +110,18 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
'company_id': self.env.company.id,
})
self.env['stock.inventory.line'].create([
{
'inventory_id': inventory.id,
'location_id': self.stock_location.id,
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product2.id,
'prod_lot_id': lot1.id,
'product_qty': 1
},
{
'inventory_id': inventory.id,
'inventory_quantity': 1,
'location_id': self.stock_location.id,
'lot_id': lot1.id
}).action_apply_inventory()
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product2.id,
'prod_lot_id': lot2.id,
'product_qty': 1
},
])
inventory.action_validate()
'inventory_quantity': 1,
'location_id': self.stock_location.id,
'lot_id': lot2.id
}).action_apply_inventory()
# create pos order with the two SN created before
+11 -27
View File
@@ -20,8 +20,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
cls.StockMove = cls.env['stock.move']
cls.UoM = cls.env['uom.uom']
cls.MrpProduction = cls.env['mrp.production']
cls.Inventory = cls.env['stock.inventory']
cls.InventoryLine = cls.env['stock.inventory.line']
cls.Quant = cls.env['stock.quant']
cls.ProductCategory = cls.env['product.category']
cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit')
@@ -405,19 +404,11 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
# Need 20 kg product c to produce 20 unit product D.
# --------------------------------------------------
inventory = self.Inventory.create({
'name': 'Inventory Product KG',
'product_ids': [(4, product_c.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
self.InventoryLine.create({
'inventory_id': inventory.id,
'product_id': product_c.id,
'product_uom_id': self.uom_kg.id,
'product_qty': 20,
'location_id': self.company_data['default_warehouse'].lot_stock_id.id})
inventory.action_validate()
self.Quant.with_context(inventory_mode=True).create({
'product_id': product_c.id, # uom = uom_kg
'inventory_quantity': 20,
'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
}).action_apply_inventory()
# --------------------------------------------------
# Assign product c to manufacturing order of product D.
@@ -453,19 +444,12 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
# ------------------------------
# Need product C ( 20 kg + 6 kg + 1502.5 gm = 27.5025 kg)
# -------------------------------------------------------
inventory = self.Inventory.create({
'name': 'Inventory Product C KG',
'product_ids': [(4, product_c.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
self.InventoryLine.create({
'inventory_id': inventory.id,
'product_id': product_c.id,
'product_uom_id': self.uom_kg.id,
'product_qty': 27.5025,
'location_id': self.company_data['default_warehouse'].lot_stock_id.id})
inventory.action_validate()
self.Quant.with_context(inventory_mode=True).create({
'product_id': product_c.id, # uom = uom_kg
'inventory_quantity': 27.51, # round up due to kg.rounding = 0.01
'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
}).action_apply_inventory()
# Assign product to manufacturing order of product A.
# ---------------------------------------------------
+9 -17
View File
@@ -134,30 +134,22 @@
<value eval="{'scheduled_date': (datetime.now() + relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')}"/>
</function>
<record id="stock_inventory_customizable_desk_update" model="stock.inventory">
<field name="name">Inventory for new Customizable Desks</field>
</record>
<record id="stock_inventory_line_7e" model="stock.inventory.line">
<!-- Inventory for new Customizable Desks -->
<record id="stock_inventory_7e" model="stock.quant">
<field name="product_id" ref="sale.product_product_4e"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_customizable_desk_update"/>
<field name="product_qty">65.0</field>
<field name="inventory_quantity">65.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_7f" model="stock.inventory.line">
<record id="stock_inventory_7f" model="stock.quant">
<field name="product_id" ref="sale.product_product_4f"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_customizable_desk_update"/>
<field name="product_qty">70.0</field>
<field name="inventory_quantity">70.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<function model="stock.inventory" name="action_start">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_customizable_desk_update'))]]" model="stock.inventory" name="search"/>
</function>
<function model="stock.inventory" name="action_validate">
<function eval="[[('state','=','confirm'),('id', '=', ref('stock_inventory_customizable_desk_update'))]]" model="stock.inventory" name="search"/>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_7e'),
ref('stock_inventory_7f'),
))]]" model="stock.quant" name="search"/>
</function>
</data>
</odoo>
@@ -22,19 +22,11 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
})
def _inv_adj_two_units(self):
inventory = self.env['stock.inventory'].create({
'name': 'test',
'location_ids': [(4, self.company_data['default_warehouse'].lot_stock_id.id)],
'product_ids': [(4, self.product.id)],
})
inventory.action_start()
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product.id, # tracking serial
'inventory_quantity': 2,
'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_id': self.product.id,
'product_qty': 2,
})
inventory.action_validate()
}).action_apply_inventory()
def _so_and_confirm_two_units(self):
sale_order = self.env['sale.order'].create({
+3 -3
View File
@@ -50,8 +50,10 @@
'wizard/stock_picking_return_views.xml',
'wizard/stock_scheduler_compute_views.xml',
'wizard/stock_immediate_transfer_views.xml',
'wizard/stock_inventory_conflict.xml',
'wizard/stock_backorder_confirmation_views.xml',
'wizard/stock_quantity_history.xml',
'wizard/stock_request_count.xml',
'wizard/stock_rules_report_views.xml',
'wizard/stock_warn_insufficient_qty_views.xml',
'wizard/product_replenish_views.xml',
@@ -65,7 +67,6 @@
'views/stock_production_lot_views.xml',
'views/stock_picking_views.xml',
'views/stock_scrap_views.xml',
'views/stock_inventory_views.xml',
'views/stock_quant_views.xml',
'views/stock_location_views.xml',
'views/stock_warehouse_views.xml',
@@ -98,10 +99,9 @@
'web.assets_backend': [
'stock/static/src/js/inventory_report_list_controller.js',
'stock/static/src/js/inventory_report_list_view.js',
'stock/static/src/js/inventory_singleton_list_renderer.js',
'stock/static/src/js/inventory_singleton_list_controller.js',
'stock/static/src/js/inventory_singleton_list_view.js',
'stock/static/src/js/inventory_validate_button_controller.js',
'stock/static/src/js/inventory_validate_button_view.js',
'stock/static/src/js/report_stock_forecasted.js',
'stock/static/src/js/stock_orderpoint_list_controller.js',
'stock/static/src/js/stock_orderpoint_list_model.js',
+56 -92
View File
@@ -12,136 +12,109 @@
</record>
<!-- Resource: stock.inventory -->
<!-- Resource: stock.quant, i.e. initial inventory -->
<record id="stock_inventory_0" model="stock.inventory">
<field name="name">Starting Inventory</field>
</record>
<!-- Resource: stock.inventory.line -->
<record id="stock_inventory_line_1" model="stock.inventory.line">
<record id="stock_inventory_1" model="stock.quant">
<field name="product_id" ref="product.product_product_24"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">16.0</field>
<field name="inventory_quantity">16.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_2" model="stock.inventory.line">
<record id="stock_inventory_2" model="stock.quant">
<field name="product_id" ref="product.product_product_7"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">18.0</field>
<field name="inventory_quantity">18.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_3" model="stock.inventory.line">
<record id="stock_inventory_3" model="stock.quant">
<field name="product_id" ref="product.product_product_6"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">500.0</field>
<field name="inventory_quantity">500.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_4" model="stock.inventory.line">
<record id="stock_inventory_4" model="stock.quant">
<field name="product_id" ref="product.product_product_9"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">22.0</field>
<field name="inventory_quantity">22.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_5" model="stock.inventory.line">
<record id="stock_inventory_5" model="stock.quant">
<field name="product_id" ref="product.product_product_10"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">33.0</field>
<field name="inventory_quantity">33.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_6" model="stock.inventory.line">
<record id="stock_inventory_6" model="stock.quant">
<field name="product_id" ref="product.product_product_11"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">26.0</field>
<field name="inventory_quantity">26.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_6b" model="stock.inventory.line">
<record id="stock_inventory_6b" model="stock.quant">
<field name="product_id" ref="product.product_product_11b"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">30.0</field>
<field name="inventory_quantity">30.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_7" model="stock.inventory.line">
<record id="stock_inventory_7" model="stock.quant">
<field name="product_id" ref="product.product_product_4"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">45.0</field>
<field name="inventory_quantity">45.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_7b" model="stock.inventory.line">
<record id="stock_inventory_7b" model="stock.quant">
<field name="product_id" ref="product.product_product_4b"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">50.0</field>
<field name="inventory_quantity">50.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_7c" model="stock.inventory.line">
<record id="stock_inventory_7c" model="stock.quant">
<field name="product_id" ref="product.product_product_4c"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">55.0</field>
<field name="inventory_quantity">55.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_7d" model="stock.inventory.line">
<record id="stock_inventory_7d" model="stock.quant">
<field name="product_id" ref="product.product_product_4d"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">60.0</field>
<field name="inventory_quantity">60.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_11" model="stock.inventory.line">
<record id="stock_inventory_11" model="stock.quant">
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">10.0</field>
<field name="inventory_quantity">10.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_12" model="stock.inventory.line">
<record id="stock_inventory_12" model="stock.quant">
<field name="product_id" ref="product.product_product_13"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">2.0</field>
<field name="inventory_quantity">2.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_13" model="stock.inventory.line">
<record id="stock_inventory_13" model="stock.quant">
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">80.0</field>
<field name="prod_lot_id" ref="lot_product_27"/>
<field name="inventory_quantity">80.0</field>
<field name="lot_id" ref="lot_product_27"/>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_14" model="stock.inventory.line">
<record id="stock_inventory_14" model="stock.quant">
<field name="product_id" ref="product.product_product_3"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">60.0</field>
<field name="inventory_quantity">60.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<record id="stock_inventory_line_15" model="stock.inventory.line">
<record id="stock_inventory_15" model="stock.quant">
<field name="product_id" ref="product.product_product_25"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_0"/>
<field name="product_qty">16.0</field>
<field name="inventory_quantity">16.0</field>
<field name="location_id" model="stock.location" eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
</record>
<function model="stock.inventory" name="_action_start">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_0'))]]" model="stock.inventory" name="search"/>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_1'),
ref('stock_inventory_2'),
ref('stock_inventory_3'),
ref('stock_inventory_4'),
ref('stock_inventory_5'),
ref('stock_inventory_6'),
ref('stock_inventory_6b'),
ref('stock_inventory_7'),
ref('stock_inventory_7b'),
ref('stock_inventory_7c'),
ref('stock_inventory_7d'),
ref('stock_inventory_11'),
ref('stock_inventory_12'),
ref('stock_inventory_13'),
ref('stock_inventory_14'),
ref('stock_inventory_15'),
))]]" model="stock.quant" name="search"/>
</function>
<function model="stock.inventory" name="action_validate">
<function eval="[[('state','=','confirm'),('id', '=', ref('stock_inventory_0'))]]" model="stock.inventory" name="search"/>
</function>
<!-- Multi Company -->
@@ -201,16 +174,10 @@
</record>
<!-- Inventory for Chicago Warehouse -->
<record id="stock_inventory_chic_0" model="stock.inventory">
<field name="name">Starting Inventory</field>
<field name="company_id" ref="stock.res_company_1"/>
</record>
<record id="stock_inventory_line_1" model="stock.inventory.line">
<record id="stock_inventory_16" model="stock.quant">
<field name="product_id" ref="product.product_product_6"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_chic_0"/>
<field name="product_qty">200.0</field>
<field name="inventory_quantity">200.0</field>
<field name="location_id" model="stock.location"
eval="obj().env['stock.location'].search([
('company_id', '=', obj().env.ref('stock.res_company_1').id),
@@ -220,11 +187,8 @@
/>
</record>
<function model="stock.inventory" name="_action_start">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_chic_0'))]]" model="stock.inventory" name="search"/>
</function>
<function model="stock.inventory" name="action_validate">
<function eval="[[('state','=','confirm'),('id', '=', ref('stock_inventory_chic_0'))]]" model="stock.inventory" name="search"/>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', '=', (ref('stock_inventory_16')))]]" model="stock.quant" name="search"/>
</function>
<!-- Activate Lots options as demo data depends on it -->
+12 -15
View File
@@ -45,11 +45,6 @@
<field name="company_id" ref="base.main_company"/>
</record>
<record id="stock_inventory_icecream" model="stock.inventory">
<field name="name">Inventory for Cable Management Box</field>
<field name="product_ids" eval="[(4, ref('stock.product_cable_management_box'))]"/>
</record>
<record id="lot_product_product_cable_management_box_0" model="stock.production.lot">
<field name="name">CM-BOX-00001</field>
<field name="product_id" ref="stock.product_cable_management_box"/>
@@ -62,24 +57,26 @@
<field name="company_id" ref="base.main_company"/>
</record>
<record id="stock_inventory_line_icecream_lot0" model="stock.inventory.line">
<record id="stock_inventory_icecream_lot_0" model="stock.quant">
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock.stock_inventory_icecream"/>
<field name="product_qty">50.0</field>
<field name="inventory_quantity">50.0</field>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="prod_lot_id" ref="lot_product_product_cable_management_box_0"/>
<field name="lot_id" ref="lot_product_product_cable_management_box_0"/>
</record>
<record id="stock_inventory_line_icecream_lot1" model="stock.inventory.line">
<record id="stock_inventory_icecream_lot_1" model="stock.quant">
<field name="product_id" ref="stock.product_cable_management_box"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock.stock_inventory_icecream"/>
<field name="product_qty">40.0</field>
<field name="inventory_quantity">40.0</field>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="prod_lot_id" ref="lot_product_product_cable_management_box_1"/>
<field name="lot_id" ref="lot_product_product_cable_management_box_1"/>
</record>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_icecream_lot_0'),
ref('stock_inventory_icecream_lot_1')
))]]" model="stock.quant" name="search"/>
</function>
<!-- Create STOCK_MOVE for OUT -->
<record id="outgoing_shipment_main_warehouse" model="stock.picking">
<field name="picking_type_id" ref="stock.picking_type_out"/>
-1
View File
@@ -6,7 +6,6 @@ from . import product_strategy
from . import res_company
from . import res_partner
from . import res_config_settings
from . import stock_inventory
from . import stock_location
from . import stock_move
from . import stock_move_line
+1 -1
View File
@@ -849,7 +849,7 @@ class ProductTemplate(models.Model):
'stock.group_stock_multi_locations',
'stock.group_production_lot',
'stock.group_tracking_owner',
'product.group_stock_packaging'
'product.group_tracking_lot'
]
if (self.env.user.user_has_groups(','.join(advanced_option_groups))):
return self.action_open_quants()
+20 -1
View File
@@ -22,6 +22,26 @@ class Company(models.Model):
domain="[('model', '=', 'stock.picking')]",
default=_default_confirmation_mail_template,
help="Email sent to the customer once the order is done.")
annual_inventory_month = fields.Selection([
('1', 'January'),
('2', 'February'),
('3', 'March'),
('4', 'April'),
('5', 'May'),
('6', 'June'),
('7', 'July'),
('8', 'August'),
('9', 'September'),
('10', 'October'),
('11', 'November'),
('12', 'December'),
], string='Annual Inventory Month',
default='12',
help="Annual inventory month for products not in a location with a cyclic inventory date. Set to no month if no automatic annual inventory.")
annual_inventory_day = fields.Integer(
string='Day of the month', default=31,
help="""Day of the month when the annual inventory should occur. If zero or negative, then the first day of the month will be selected instead.
If greater than the last day of a month, then the last day of the month will be selected instead.""")
def _create_transit_location(self):
'''Create a transit location with company_id being the given company_id. This is needed
@@ -77,7 +97,6 @@ class Company(models.Model):
company.id,
)
def _create_scrap_location(self):
parent_location = self.env.ref('stock.stock_location_locations_virtual', raise_if_not_found=False)
for company in self:
+4 -1
View File
@@ -39,7 +39,10 @@ class ResConfigSettings(models.TransientModel):
module_quality_control_worksheet = fields.Boolean("Quality Worksheet")
group_stock_multi_locations = fields.Boolean('Storage Locations', implied_group='stock.group_stock_multi_locations',
help="Store products in specific locations of your warehouse (e.g. bins, racks) and to track inventory accordingly.")
group_stock_storage_categories = fields.Boolean('Storage Categories', implied_group='stock.group_stock_storage_categories')
group_stock_storage_categories = fields.Boolean(
'Storage Categories', implied_group='stock.group_stock_storage_categories')
annual_inventory_month = fields.Selection(related='company_id.annual_inventory_month', readonly=False)
annual_inventory_day = fields.Integer(related='company_id.annual_inventory_day', readonly=False)
@api.onchange('group_stock_multi_locations')
def _onchange_group_stock_multi_locations(self):
-767
View File
@@ -1,767 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _, api, fields, models
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import float_compare, float_is_zero
class Inventory(models.Model):
_name = "stock.inventory"
_description = "Inventory"
_order = "date desc, id desc"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(
'Inventory Reference', default="Inventory",
readonly=True, required=True,
states={'draft': [('readonly', False)]})
date = fields.Datetime(
'Inventory Date',
readonly=True, required=True,
default=fields.Datetime.now,
help="If the inventory adjustment is not validated, date at which the theoritical quantities have been checked.\n"
"If the inventory adjustment is validated, date at which the inventory adjustment has been validated.")
line_ids = fields.One2many(
'stock.inventory.line', 'inventory_id', string='Inventories',
copy=False, readonly=False,
states={'done': [('readonly', True)]})
move_ids = fields.One2many(
'stock.move', 'inventory_id', string='Created Moves',
states={'done': [('readonly', True)]})
state = fields.Selection(string='Status', selection=[
('draft', 'Draft'),
('cancel', 'Cancelled'),
('confirm', 'In Progress'),
('done', 'Validated')],
copy=False, index=True, readonly=True, tracking=True,
default='draft')
company_id = fields.Many2one(
'res.company', 'Company',
readonly=True, index=True, required=True,
states={'draft': [('readonly', False)]},
default=lambda self: self.env.company)
location_ids = fields.Many2many(
'stock.location', string='Locations',
readonly=True, check_company=True,
states={'draft': [('readonly', False)]},
domain="[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit'])]")
product_ids = fields.Many2many(
'product.product', string='Products', check_company=True,
domain="[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", readonly=True,
states={'draft': [('readonly', False)]},
help="Specify Products to focus your inventory on particular Products.")
start_empty = fields.Boolean('Empty Inventory',
help="Allows to start with an empty inventory.")
prefill_counted_quantity = fields.Selection(string='Counted Quantities',
help="Allows to start with a pre-filled counted quantity for each lines or "
"with all counted quantities set to zero.", default='counted',
selection=[('counted', 'Default to stock on hand'), ('zero', 'Default to zero')])
exhausted = fields.Boolean(
'Include Exhausted Products', readonly=True,
states={'draft': [('readonly', False)]},
help="Include also products with quantity of 0")
is_conflict_inventory = fields.Boolean(string="Is Auto-generated From Conflict", readonly=True,
help="Technical flag to indicate this inventory was auto-generated due to a conflicting inventory. This allows us to auto-add/remove products when inventory is still in draft.")
lot_ids = fields.Many2many('stock.production.lot', string='Duplicate Serial Numbers', readonly=True,
help="Technical field to support auto-generated conflicting inventory from a duplicated SN. This value is expected to never be viewed/edited except by conflicting inventory checks.")
@api.onchange('company_id')
def _onchange_company_id(self):
# If the multilocation group is not active, default the location to the one of the main
# warehouse.
if not self.user_has_groups('stock.group_stock_multi_locations'):
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1)
if warehouse:
self.location_ids = warehouse.lot_stock_id
def copy_data(self, default=None):
name = _("%s (copy)") % (self.name)
default = dict(default or {}, name=name)
return super(Inventory, self).copy_data(default)
@api.ondelete(at_uninstall=False)
def _unlink_if_draft_or_cancel(self):
for inventory in self:
if inventory.state not in ('draft', 'cancel'):
raise UserError(_('You can only delete a draft inventory adjustment. If the inventory adjustment is not done, you can cancel it.'))
def action_validate(self):
if not self.exists():
return
self.ensure_one()
if not self.user_has_groups('stock.group_stock_manager'):
raise UserError(_("Only a stock manager can validate an inventory adjustment."))
if self.state != 'confirm':
raise UserError(_(
"You can't validate the inventory '%s', maybe this inventory "
"has been already validated or isn't ready.", self.name))
inventory_lines = self.line_ids.filtered(lambda l: l.product_id.tracking in ['lot', 'serial'] and not l.prod_lot_id and l.theoretical_qty != l.product_qty)
lines = self.line_ids.filtered(lambda l: float_compare(l.product_qty, 1, precision_rounding=l.product_uom_id.rounding) > 0 and l.product_id.tracking == 'serial' and l.prod_lot_id)
if inventory_lines and not lines:
wiz_lines = [(0, 0, {'product_id': product.id, 'tracking': product.tracking}) for product in inventory_lines.mapped('product_id')]
wiz = self.env['stock.track.confirmation'].create({'inventory_id': self.id, 'tracking_line_ids': wiz_lines})
return {
'name': _('Tracked Products in Inventory Adjustment'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'views': [(False, 'form')],
'res_model': 'stock.track.confirmation',
'target': 'new',
'res_id': wiz.id,
}
self._action_done()
self.line_ids._check_company()
self._check_company()
return True
def _action_done(self):
negative = next((line for line in self.mapped('line_ids') if line.product_qty < 0 and line.product_qty != line.theoretical_qty), False)
if negative:
raise UserError(_(
'You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s',
negative.product_id.display_name,
negative.product_qty
))
self.action_check()
self.write({'state': 'done', 'date': fields.Datetime.now()})
self.post_inventory()
if self.user_has_groups('stock.group_stock_multi_locations') and not self.product_ids and self.location_ids:
locations = self.env['stock.location'].with_context(active_test=False).search([('id', 'child_of', self.location_ids.ids), ('usage', 'in', ['internal', 'transit'])])
locations.last_inventory_date = fields.Datetime.now()
return True
def post_inventory(self):
# The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory
# as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior
# as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want).
self.mapped('move_ids').filtered(lambda move: move.state != 'done')._action_done()
return True
def action_check(self):
""" Checks the inventory and computes the stock move to do """
# tde todo: clean after _generate_moves
for inventory in self.filtered(lambda x: x.state not in ('done','cancel')):
# first remove the existing stock moves linked to this inventory
inventory.with_context(prefetch_fields=False).mapped('move_ids').unlink()
inventory.line_ids._generate_moves()
def action_cancel_draft(self):
self.mapped('move_ids')._action_cancel()
self.line_ids.unlink()
self.write({'state': 'draft'})
def action_start(self):
self.ensure_one()
self._action_start()
self._check_company()
return self.action_open_inventory_lines()
def _action_start(self):
""" Confirms the Inventory Adjustment and generates its inventory lines
if its state is draft and don't have already inventory lines (can happen
with demo data or tests).
"""
for inventory in self:
if inventory.state != 'draft':
continue
vals = {
'state': 'confirm',
'date': fields.Datetime.now()
}
if not inventory.line_ids and not inventory.start_empty:
self.env['stock.inventory.line'].create(inventory._get_inventory_lines_values())
inventory.write(vals)
def action_open_inventory_lines(self):
self.ensure_one()
action = {
'type': 'ir.actions.act_window',
'view_mode': 'tree',
'name': _('Inventory Lines'),
'res_model': 'stock.inventory.line',
}
context = {
'default_is_editable': True,
'default_inventory_id': self.id,
'default_company_id': self.company_id.id,
}
# Define domains and context
domain = [
('inventory_id', '=', self.id),
('location_id.usage', 'in', ['internal', 'transit'])
]
if self.location_ids:
context['default_location_id'] = self.location_ids[0].id
if len(self.location_ids) == 1:
if not self.location_ids[0].child_ids:
context['readonly_location_id'] = True
if self.product_ids:
# no_create on product_id field
action['view_id'] = self.env.ref('stock.stock_inventory_line_tree_no_product_create').id
if len(self.product_ids) == 1:
context['default_product_id'] = self.product_ids[0].id
else:
# no product_ids => we're allowed to create new products in tree
action['view_id'] = self.env.ref('stock.stock_inventory_line_tree').id
action['context'] = context
action['domain'] = domain
return action
def action_view_related_move_lines(self):
self.ensure_one()
domain = [('move_id', 'in', self.move_ids.ids)]
action = {
'name': _('Product Moves'),
'type': 'ir.actions.act_window',
'res_model': 'stock.move.line',
'view_type': 'list',
'view_mode': 'list,form',
'domain': domain,
}
return action
def action_print(self):
return self.env.ref('stock.action_report_inventory').report_action(self)
def _get_quantities(self):
"""Return quantities group by product_id, location_id, lot_id, package_id and owner_id
:return: a dict with keys as tuple of group by and quantity as value
:rtype: dict
"""
self.ensure_one()
if self.location_ids:
domain_loc = [('id', 'child_of', self.location_ids.ids)]
else:
domain_loc = [('company_id', '=', self.company_id.id), ('usage', 'in', ['internal', 'transit'])]
locations_ids = [l['id'] for l in self.env['stock.location'].search_read(domain_loc, ['id'])]
domain = [('company_id', '=', self.company_id.id),
('location_id', 'in', locations_ids)]
if self.prefill_counted_quantity == 'zero':
domain.append(('product_id.active', '=', True))
if self.lot_ids:
domain = expression.AND([domain, [('lot_id', 'in', self.lot_ids.ids)]])
if self.is_conflict_inventory and not self.lot_ids:
domain = expression.AND([domain, [('quantity', '<', 0)]])
else:
domain.append(('quantity', '!=', 0))
if self.product_ids:
domain = expression.AND([domain, [('product_id', 'in', self.product_ids.ids)]])
fields = ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'quantity:sum']
group_by = ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id']
quants = self.env['stock.quant'].read_group(domain, fields, group_by, lazy=False)
return {(
quant['product_id'] and quant['product_id'][0] or False,
quant['location_id'] and quant['location_id'][0] or False,
quant['lot_id'] and quant['lot_id'][0] or False,
quant['package_id'] and quant['package_id'][0] or False,
quant['owner_id'] and quant['owner_id'][0] or False):
quant['quantity'] for quant in quants
}
def _get_exhausted_inventory_lines_vals(self, non_exhausted_set):
"""Return the values of the inventory lines to create if the user
wants to include exhausted products. Exhausted products are products
without quantities or quantity equal to 0.
:param non_exhausted_set: set of tuple (product_id, location_id) of non exhausted product-location
:return: a list containing the `stock.inventory.line` values to create
:rtype: list
"""
self.ensure_one()
if self.product_ids:
product_ids = self.product_ids.ids
else:
product_ids = self.env['product.product'].search_read([
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
('type', '=', 'product'),
('active', '=', True)], ['id'])
product_ids = [p['id'] for p in product_ids]
if self.location_ids:
location_ids = self.location_ids.ids
else:
location_ids = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)]).lot_stock_id.ids
vals = []
for product_id in product_ids:
for location_id in location_ids:
if ((product_id, location_id) not in non_exhausted_set):
vals.append({
'inventory_id': self.id,
'product_id': product_id,
'location_id': location_id,
'theoretical_qty': 0
})
return vals
def _get_inventory_lines_values(self):
"""Return the values of the inventory lines to create for this inventory.
:return: a list containing the `stock.inventory.line` values to create
:rtype: list
"""
self.ensure_one()
quants_groups = self._get_quantities()
vals = []
for (product_id, location_id, lot_id, package_id, owner_id), quantity in quants_groups.items():
line_values = {
'inventory_id': self.id,
'product_qty': 0 if self.prefill_counted_quantity == "zero" else quantity,
'theoretical_qty': quantity,
'prod_lot_id': lot_id,
'partner_id': owner_id,
'product_id': product_id,
'location_id': location_id,
'package_id': package_id
}
line_values['product_uom_id'] = self.env['product.product'].browse(product_id).uom_id.id
vals.append(line_values)
if self.exhausted:
vals += self._get_exhausted_inventory_lines_vals({(l['product_id'], l['location_id']) for l in vals})
return vals
@api.model
def _run_inventory_tasks(self, company_id=False):
""" Run conflict inventory tasks and generate/clean up cyclic inventories for locations as follows:
- [to create]: - The location has a cyclic count set (i.e. inventory every XX days) and has a next_inventory_date of today or earlier
- The location is not a descendant of another location that is having a cyclic inventory created
- The location doesn't already have an in progress inventory specific to (i.e. only for) it
- [to delete]: - Existing draft cyclic inventory that was created before today (i.e. still draft) [new one created afterwards]
- Existing draft location inventory that is a descendant of a location that has a next_inventory_date of today or earlier
"""
# if cron triggered => apply to all companies
company_domain = [('company_id', '!=', False)]
if company_id:
# if manually triggered => only apply to user's companies
company_domain = [('company_id', '=', company_id)]
# cyclic location inventories
domain = expression.AND([[('next_inventory_date', '<=', fields.Date.today())], company_domain])
locations = self.env['stock.location'].search(domain)
if locations:
# ignore any locations that are children of other locations that should have an inventory done
locations = locations.filtered(lambda l: not any(int(location_id) in locations.ids for location_id in l.parent_path.split('/')[:-2]))
existing_loc_invs = self.search([('state', 'in', ['draft', 'confirm']), ('product_ids', '=', False), ('location_ids', '!=', False)])
invs_to_unlink = self.env['stock.inventory']
existing_cyclic_inv = self.env['stock.inventory']
for inventory in existing_loc_invs:
# assume inventories with more than 1 location were manually created and should be untouched
if len(inventory.location_ids) == 1:
if inventory.state == 'draft' \
and (any(int(location_id) in locations.ids for location_id in inventory.location_ids[0].parent_path.split('/')[:-2])
or (inventory.location_ids[0] in locations and inventory.create_date < fields.Datetime.today())):
invs_to_unlink |= inventory
elif inventory.location_ids[0] in locations:
existing_cyclic_inv |= inventory
invs_to_unlink.unlink()
locations -= existing_cyclic_inv.mapped('location_ids')
location_vals = []
for location in locations:
location_vals.append({'name': "Reoccuring Inventory for: " + str(location.name),
'company_id': location.company_id.id,
'location_ids': location})
self.create(location_vals)
# conflict inventories handled in separate function so inventories can also be updated when user opens inventory menuitem
self._run_conflict_inventory_tasks(company_id=company_id)
@api.model
def _run_conflict_inventory_tasks(self, company_id=False):
""" Updates/creates/deletes conflict inventories. Conflict inventories include:
- negative quantities values, created per warehouse
Note that an in progress inventory will prevent an update/unlink when its
corresponding values are out of date.
"""
company_domain = [('company_id', '!=', False)]
if company_id:
# if manually triggered => only apply to user's companies
company_domain = [('company_id', '=', company_id)]
# negative quantity check
inventory_vals_to_create = []
updated_invs = self.env['stock.inventory']
existing_conflict_invs = self.search(expression.AND([
[
('state', 'in', ['draft', 'confirm']),
('is_conflict_inventory', '=', True)
], company_domain]))
neg_quants = self.env['stock.quant'].search(expression.AND([
[
('quantity', '<', 0.0),
('location_id.usage', 'in', ['internal', 'transit'])
], company_domain]))
company_ids = neg_quants.mapped('company_id')
warehouse_ids = self.env['stock.warehouse'].search([('company_id', 'in', company_ids.ids)])
warehouse_locations = self.env['stock.location'].search([('id', 'child_of', warehouse_ids.mapped('view_location_id').ids)])
company_to_warehouses = defaultdict(lambda: self.env['stock.warehouse'])
warehouse_to_locations = defaultdict(lambda: self.env['stock.location'])
for warehouse in warehouse_ids:
company_to_warehouses[warehouse.company_id] |= warehouse
warehouse_to_locations[warehouse] = warehouse_locations.filtered(lambda l: any(int(location_id) == warehouse.view_location_id.id for location_id in l.parent_path.split('/')[:-2]))
for company_id in company_ids:
# separate auto-generated inventories by warehouse
for warehouse in company_to_warehouses[company_id]:
# avoid conflicting inventories! Wait until the next time this is run after the previous neg qty inventory is completed
if existing_conflict_invs.filtered(lambda i: i.state == 'confirm' and i.company_id == company_id and i.location_ids & warehouse_to_locations[warehouse]):
continue
warehouse_quants = neg_quants.filtered(lambda q: q.location_id in warehouse_to_locations[warehouse])
if warehouse_quants:
draft_inv = existing_conflict_invs.filtered(lambda i: i.state == 'draft' and i.company_id == company_id and i.location_ids & warehouse_to_locations[warehouse])
if draft_inv:
# only write in in first draft in case there are duplicates due to function being called while its already
draft_inv[0].write({'product_ids': warehouse_quants.mapped('product_id')})
updated_invs |= draft_inv[0]
else:
inventory_vals_to_create.append({
'name': "Negative Quantity Inventory: " + warehouse.name,
'company_id': company_id.id,
'product_ids': warehouse_quants.mapped('product_id'),
'is_conflict_inventory': True,
'location_ids': warehouse.view_location_id.child_ids
})
# conflicting SN check
domain = expression.AND([[('location_id.usage', 'in', ['internal', 'transit']),
('lot_id', '!=', False),
('product_id.tracking', '=', 'serial'),
('quantity', '!=', 0.0)],
company_domain])
quants = self.env['stock.quant'].read_group(domain, ['lot_id', 'company_id', 'product_id'], ['lot_id', 'company_id', 'product_id'], lazy=False)
company_to_sn_conflicts = defaultdict(lambda: ([], []))
for quant in quants:
if quant['__count'] > 1:
company_to_sn_conflicts[quant['company_id'][0]][0].append(quant['lot_id'][0])
company_to_sn_conflicts[quant['company_id'][0]][1].append(quant['product_id'][0])
for company_id, (lot_ids, product_ids) in company_to_sn_conflicts.items():
# avoid conflicting inventories! Wait until the next time this is run after the previous conflicting SN inventory is completed
if existing_conflict_invs.filtered(lambda i: i.state == 'confirm' and i.lot_ids and i.company_id.id == company_id):
continue
draft_inv = existing_conflict_invs.filtered(lambda i: i.state == 'draft' and i.lot_ids and i.company_id.id == company_id)
if draft_inv:
draft_inv.write({'lot_ids': lot_ids,
'product_ids': product_ids})
updated_invs |= draft_inv
else:
inventory_vals_to_create.append({
'name': "Duplicate SN Inventory",
'company_id': company_id,
'lot_ids': lot_ids,
'product_ids': product_ids,
'is_conflict_inventory': True
})
self.create(inventory_vals_to_create)
# remove all obsolete conflict inventories
(existing_conflict_invs.filtered(lambda i: i.state == 'draft') - updated_invs).unlink()
@api.model
def action_open_inventory_view(self):
self._run_conflict_inventory_tasks()
return self.env["ir.actions.actions"]._for_xml_id("stock.action_inventory_form")
class InventoryLine(models.Model):
_name = "stock.inventory.line"
_description = "Inventory Line"
_order = "product_id, inventory_id, location_id, prod_lot_id"
@api.model
def _domain_location_id(self):
if self.env.context.get('active_model') == 'stock.inventory':
inventory = self.env['stock.inventory'].browse(self.env.context.get('active_id'))
if inventory.exists() and inventory.location_ids:
return "[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit']), ('id', 'child_of', %s)]" % inventory.location_ids.ids
return "[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit'])]"
@api.model
def _domain_product_id(self):
if self.env.context.get('active_model') == 'stock.inventory':
inventory = self.env['stock.inventory'].browse(self.env.context.get('active_id'))
if inventory.exists() and len(inventory.product_ids) > 1:
return "[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id), ('id', 'in', %s)]" % inventory.product_ids.ids
return "[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]"
is_editable = fields.Boolean(help="Technical field to restrict editing.")
inventory_id = fields.Many2one(
'stock.inventory', 'Inventory', check_company=True,
index=True, ondelete='cascade')
partner_id = fields.Many2one('res.partner', 'Owner', check_company=True)
product_id = fields.Many2one(
'product.product', 'Product', check_company=True,
domain=lambda self: self._domain_product_id(),
index=True, required=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Product Unit of Measure',
required=True, readonly=True)
product_qty = fields.Float(
'Counted Quantity',
readonly=True, states={'confirm': [('readonly', False)]},
digits='Product Unit of Measure', default=0)
categ_id = fields.Many2one(related='product_id.categ_id', store=True)
location_id = fields.Many2one(
'stock.location', 'Location', check_company=True,
domain=lambda self: self._domain_location_id(),
index=True, required=True)
package_id = fields.Many2one(
'stock.quant.package', 'Pack', index=True, check_company=True,
domain="[('location_id', '=', location_id)]",
)
prod_lot_id = fields.Many2one(
'stock.production.lot', 'Lot/Serial Number', check_company=True,
domain="[('product_id','=',product_id), ('company_id', '=', company_id)]")
company_id = fields.Many2one(
'res.company', 'Company', related='inventory_id.company_id',
index=True, readonly=True, store=True)
state = fields.Selection(string='Status', related='inventory_id.state')
theoretical_qty = fields.Float(
'Theoretical Quantity',
digits='Product Unit of Measure', readonly=True)
difference_qty = fields.Float('Difference', compute='_compute_difference',
help="Indicates the gap between the product's theoretical quantity and its newest quantity.",
readonly=True, digits='Product Unit of Measure', search="_search_difference_qty")
inventory_date = fields.Datetime('Inventory Date', readonly=True,
default=fields.Datetime.now,
help="Last date at which the On Hand Quantity has been computed.")
outdated = fields.Boolean(string='Quantity outdated',
compute='_compute_outdated', search='_search_outdated')
product_tracking = fields.Selection(string='Tracking', related='product_id.tracking', readonly=True)
@api.depends('product_qty', 'theoretical_qty')
def _compute_difference(self):
for line in self:
line.difference_qty = line.product_qty - line.theoretical_qty
@api.depends('inventory_date', 'product_id.stock_move_ids', 'theoretical_qty', 'product_uom_id.rounding')
def _compute_outdated(self):
quants_by_inventory = {inventory: inventory._get_quantities() for inventory in self.inventory_id}
for line in self:
quants = quants_by_inventory[line.inventory_id]
if line.state == 'done' or not line.id:
line.outdated = False
continue
qty = quants.get((
line.product_id.id,
line.location_id.id,
line.prod_lot_id.id,
line.package_id.id,
line.partner_id.id), 0
)
if float_compare(qty, line.theoretical_qty, precision_rounding=line.product_uom_id.rounding) != 0:
line.outdated = True
else:
line.outdated = False
@api.onchange('product_id', 'location_id', 'product_uom_id', 'prod_lot_id', 'partner_id', 'package_id')
def _onchange_quantity_context(self):
if self.product_id:
self.product_uom_id = self.product_id.uom_id
if self.product_id and self.location_id and self.product_id.uom_id.category_id == self.product_uom_id.category_id: # TDE FIXME: last part added because crash
theoretical_qty = self.product_id.get_theoretical_quantity(
self.product_id.id,
self.location_id.id,
lot_id=self.prod_lot_id.id,
package_id=self.package_id.id,
owner_id=self.partner_id.id,
to_uom=self.product_uom_id.id,
)
else:
theoretical_qty = 0
# Sanity check on the lot.
if self.prod_lot_id:
if self.product_id.tracking == 'none' or self.product_id != self.prod_lot_id.product_id:
self.prod_lot_id = False
if self.prod_lot_id and self.product_id.tracking == 'serial':
# We force `product_qty` to 1 for SN tracked product because it's
# the only relevant value aside 0 for this kind of product.
self.product_qty = 1
elif self.product_id and float_compare(self.product_qty, self.theoretical_qty, precision_rounding=self.product_uom_id.rounding) == 0:
# We update `product_qty` only if it equals to `theoretical_qty` to
# avoid to reset quantity when user manually set it.
self.product_qty = theoretical_qty
self.theoretical_qty = theoretical_qty
@api.onchange('product_qty', 'prod_lot_id')
def _onchange_serial_number(self):
if self.prod_lot_id and self.product_id.tracking == 'serial' and not float_is_zero(self.product_qty, self.product_id.uom_id.rounding):
dupe_sn_lines = self.env['stock.inventory.line']
message = _('Is this expected? For example this can happen if pick-pack-ship was not completed in the right order. In this case the issue will be solved '
'automatically once all steps are completed. Otherwise, you can adjust quantities in these locations.')
# first check current inventory for non-zero dupe SNs
if self.inventory_id:
dupe_sn_lines |= self.inventory_id.line_ids.filtered(
lambda l: l.product_id == self.product_id
and l.prod_lot_id == self.prod_lot_id
and l.location_id.id is not self.location_id.id
and not float_is_zero(l.product_qty, l.product_id.uom_id.rounding))
if len(dupe_sn_lines) > 0:
message = _('There is already one or more inventory adjustment line(s) counting the Serial Number (%s) at the location(s): %s.\n\n',
self.prod_lot_id.name, ', '.join(dupe_sn_lines.location_id.mapped('display_name'))) + message
return {'warning': {'title': _('Warning'), 'message': message}}
# then check locations not in inventory for dupe SNs
if self.inventory_id and self.inventory_id.location_ids:
quants = self.env['stock.quant'].search([('product_id', '=', self.product_id.id),
('lot_id', '=', self.prod_lot_id.id),
('quantity', '!=', 0),
('location_id', 'not in', self.inventory_id.location_ids.ids),
'|', ('location_id.usage', '=', 'customer'),
'&', ('company_id', '=', self.company_id.id),
('location_id.usage', 'in', ('internal', 'transit'))])
if quants:
message = _('The Serial Number (%s) is already used in these location(s): %s.\n\n',
self.prod_lot_id.name, ', '.join(quants.location_id.mapped('display_name'))) + message
return {'warning': {'title': _('Warning'), 'message': message}}
@api.model_create_multi
def create(self, vals_list):
""" Override to handle the case we create inventory line without
`theoretical_qty` because this field is usually computed, but in some
case (typicaly in tests), we create inventory line without trigger the
onchange, so in this case, we set `theoretical_qty` depending of the
product's theoretical quantity.
Handles the same problem with `product_uom_id` as this field is normally
set in an onchange of `product_id`.
Finally, this override checks we don't try to create a duplicated line.
"""
for values in vals_list:
if 'theoretical_qty' not in values:
theoretical_qty = self.env['product.product'].get_theoretical_quantity(
values['product_id'],
values['location_id'],
lot_id=values.get('prod_lot_id'),
package_id=values.get('package_id'),
owner_id=values.get('partner_id'),
to_uom=values.get('product_uom_id'),
)
values['theoretical_qty'] = theoretical_qty
if 'product_id' in values and 'product_uom_id' not in values:
values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id
res = super(InventoryLine, self).create(vals_list)
res._check_no_duplicate_line()
return res
def write(self, vals):
res = super(InventoryLine, self).write(vals)
self._check_no_duplicate_line()
return res
def _check_no_duplicate_line(self):
for line in self:
domain = [
('id', '!=', line.id),
('product_id', '=', line.product_id.id),
('location_id', '=', line.location_id.id),
('partner_id', '=', line.partner_id.id),
('package_id', '=', line.package_id.id),
('prod_lot_id', '=', line.prod_lot_id.id),
('inventory_id', '=', line.inventory_id.id)]
existings = self.search_count(domain)
if existings:
raise UserError(_("There is already one inventory adjustment line for this product,"
" you should rather modify this one instead of creating a new one."))
@api.constrains('product_id')
def _check_product_id(self):
""" As no quants are created for consumable products, it should not be possible do adjust
their quantity.
"""
for line in self:
if line.product_id.type != 'product':
raise ValidationError(_("You can only adjust storable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type))
def _get_move_values(self, qty, location_id, location_dest_id, out):
self.ensure_one()
return {
'name': _('INV:') + (self.inventory_id.name or ''),
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': qty,
'date': self.inventory_id.date,
'company_id': self.inventory_id.company_id.id,
'inventory_id': self.inventory_id.id,
'state': 'confirmed',
'restrict_partner_id': self.partner_id.id,
'location_id': location_id,
'location_dest_id': location_dest_id,
'move_line_ids': [(0, 0, {
'product_id': self.product_id.id,
'lot_id': self.prod_lot_id.id,
'product_uom_qty': 0, # bypass reservation here
'product_uom_id': self.product_uom_id.id,
'qty_done': qty,
'package_id': out and self.package_id.id or False,
'result_package_id': (not out) and self.package_id.id or False,
'location_id': location_id,
'location_dest_id': location_dest_id,
'owner_id': self.partner_id.id,
})]
}
def _get_virtual_location(self):
return self.product_id.with_company(self.company_id).property_stock_inventory
def _generate_moves(self):
vals_list = []
for line in self:
virtual_location = line._get_virtual_location()
rounding = line.product_id.uom_id.rounding
if float_is_zero(line.difference_qty, precision_rounding=rounding):
continue
if line.difference_qty > 0: # found more than expected
vals = line._get_move_values(line.difference_qty, virtual_location.id, line.location_id.id, False)
else:
vals = line._get_move_values(abs(line.difference_qty), line.location_id.id, virtual_location.id, True)
vals_list.append(vals)
return self.env['stock.move'].create(vals_list)
def action_refresh_quantity(self):
filtered_lines = self.filtered(lambda l: l.state != 'done')
for line in filtered_lines:
if line.outdated:
quants = self.env['stock.quant']._gather(line.product_id, line.location_id, lot_id=line.prod_lot_id, package_id=line.package_id, owner_id=line.partner_id, strict=True)
if quants.exists():
quantity = sum(quants.mapped('quantity'))
if line.theoretical_qty != quantity:
line.theoretical_qty = quantity
else:
line.theoretical_qty = 0
line.inventory_date = fields.Datetime.now()
def action_reset_product_qty(self):
""" Write `product_qty` to zero on the selected records. """
impacted_lines = self.env['stock.inventory.line']
for line in self:
if line.state == 'done':
continue
impacted_lines |= line
impacted_lines.write({'product_qty': 0})
def _search_difference_qty(self, operator, value):
if operator == '=':
result = True
elif operator == '!=':
result = False
else:
raise NotImplementedError()
lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))])
line_ids = lines.filtered(lambda line: float_is_zero(line.difference_qty, line.product_id.uom_id.rounding) == result).ids
return [('id', 'in', line_ids)]
def _search_outdated(self, operator, value):
if operator != '=':
if operator == '!=' and isinstance(value, bool):
value = not value
else:
raise NotImplementedError()
lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))])
line_ids = lines.filtered(lambda line: line.outdated == value).ids
return [('id', 'in', line_ids)]
+35 -3
View File
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from collections import defaultdict, OrderedDict
from datetime import timedelta
@@ -63,8 +66,8 @@ class Location(models.Model):
putaway_rule_ids = fields.One2many('stock.putaway.rule', 'location_in_id', 'Putaway Rules')
barcode = fields.Char('Barcode', copy=False)
quant_ids = fields.One2many('stock.quant', 'location_id')
cyclic_inventory_frequency = fields.Integer("Inventory Frequency (Days)", default=0, help=" When different than 0, inventory adjustments for products stored at this location will be created automatically at the defined frequency.")
last_inventory_date = fields.Datetime("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.")
cyclic_inventory_frequency = fields.Integer("Inventory Frequency (Days)", default=0, help=" When different than 0, inventory count date for products stored at this location will be automatically set at the defined frequency.")
last_inventory_date = fields.Date("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.")
next_inventory_date = fields.Date("Next Expected Inventory", compute="_compute_next_inventory_date", store=True, help="Date for next planned inventory based on cyclic schedule.")
warehouse_view_ids = fields.One2many('stock.warehouse', 'view_location_id', readonly=True)
warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id')
@@ -109,7 +112,7 @@ class Location(models.Model):
if location.company_id and location.usage in ['internal', 'transit'] and location.cyclic_inventory_frequency > 0:
try:
if location.last_inventory_date:
days_until_next_inventory = location.cyclic_inventory_frequency - (fields.Date.today() - location.last_inventory_date.date()).days
days_until_next_inventory = location.cyclic_inventory_frequency - (fields.Date.today() - location.last_inventory_date).days
if days_until_next_inventory <= 0:
location.next_inventory_date = fields.Date.today() + timedelta(days=1)
else:
@@ -232,6 +235,35 @@ class Location(models.Model):
return putaway_rules._get_putaway_location(product, quantity, package, qty_by_location) or self
def _get_next_inventory_date(self):
""" Used to get the next inventory date for a quant located in this location. It is
based on:
1. Does the location have a cyclic inventory set?
2. If not 1, then is there an annual inventory date set (for its company)?
3. If not 1 and 2, then quants have no next inventory date."""
if self.usage not in ['internal', 'transit']:
return False
next_inventory_date = False
if self.next_inventory_date:
next_inventory_date = self.next_inventory_date
elif self.company_id.annual_inventory_month:
today = fields.Date.today()
annual_inventory_month = int(self.company_id.annual_inventory_month)
# Manage 0 and negative annual_inventory_day
annual_inventory_day = max(self.company_id.annual_inventory_day, 1)
max_day = calendar.monthrange(today.year, annual_inventory_month)[1]
# Manage annual_inventory_day bigger than last_day
annual_inventory_day = min(annual_inventory_day, max_day)
next_inventory_date = today.replace(
month=annual_inventory_month, day=annual_inventory_day)
if next_inventory_date <= today:
# Manage leap year with the february
max_day = calendar.monthrange(today.year + 1, annual_inventory_month)[1]
annual_inventory_day = min(annual_inventory_day, max_day)
next_inventory_date = next_inventory_date.replace(
day=annual_inventory_day, year=today.year + 1)
return next_inventory_date
def should_bypass_reservation(self):
self.ensure_one()
return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id)
+3 -3
View File
@@ -130,7 +130,7 @@ class StockMove(models.Model):
help='If checked, when this move is cancelled, cancel the linked move too')
delay_alert_date = fields.Datetime('Delay Alert Date', help='Process at this date to be on time', compute="_compute_delay_alert_date", store=True)
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', compute='_compute_picking_type_id', store=True, check_company=True)
inventory_id = fields.Many2one('stock.inventory', 'Inventory', check_company=True)
is_inventory = fields.Boolean('Inventory')
move_line_ids = fields.One2many('stock.move.line', 'move_id')
move_line_nosuggest_ids = fields.One2many('stock.move.line', 'move_id', domain=['|', ('product_qty', '=', 0.0), ('qty_done', '!=', 0.0)])
origin_returned_move_id = fields.Many2one(
@@ -1507,13 +1507,13 @@ class StockMove(models.Model):
# Cancel moves where necessary ; we should do it before creating the extra moves because
# this operation could trigger a merge of moves.
for move in moves:
if move.quantity_done <= 0:
if move.quantity_done <= 0 and not move.is_inventory:
if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0 or cancel_backorder:
move._action_cancel()
# Create extra moves where necessary
for move in moves:
if move.state == 'cancel' or move.quantity_done <= 0:
if move.state == 'cancel' or (move.quantity_done <= 0 and not move.is_inventory):
continue
moves_ids_todo |= move._create_extra_move().ids
+3 -2
View File
@@ -62,6 +62,7 @@ class StockMoveLine(models.Model):
picking_type_entire_packs = fields.Boolean(related='picking_id.picking_type_id.show_entire_packs', readonly=True)
state = fields.Selection(related='move_id.state', store=True, related_sudo=False)
is_initial_demand_editable = fields.Boolean(related='move_id.is_initial_demand_editable')
is_inventory = fields.Boolean(related='move_id.is_inventory')
is_locked = fields.Boolean(related='move_id.is_locked', default=True, readonly=True)
consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ")
produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ")
@@ -511,7 +512,7 @@ class StockMoveLine(models.Model):
# checkboxes on the picking type, he's allowed to enter tracked
# products without a `lot_id`.
continue
elif ml.move_id.inventory_id:
elif ml.is_inventory:
# If an inventory adjustment is linked, the user is allowed to enter
# tracked products without a `lot_id`.
continue
@@ -520,7 +521,7 @@ class StockMoveLine(models.Model):
ml_ids_tracked_without_lot.add(ml.id)
elif qty_done_float_compared < 0:
raise UserError(_('No negative quantities allowed'))
else:
elif not ml.is_inventory:
ml_ids_to_delete.add(ml.id)
if ml_ids_tracked_without_lot:
+202 -57
View File
@@ -8,7 +8,7 @@ from psycopg2 import Error, OperationalError
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.tools.float_utils import float_compare, float_is_zero
_logger = logging.getLogger(__name__)
@@ -80,9 +80,6 @@ class StockQuant(models.Model):
'Quantity',
help='Quantity of products in this quant, in the default unit of measure of the product',
readonly=True)
inventory_quantity = fields.Float(
'Inventoried Quantity', compute='_compute_inventory_quantity',
inverse='_set_inventory_quantity', groups='stock.group_stock_manager')
reserved_quantity = fields.Float(
'Reserved Quantity',
default=0.0,
@@ -96,39 +93,36 @@ class StockQuant(models.Model):
tracking = fields.Selection(related='product_id.tracking', readonly=True)
on_hand = fields.Boolean('On Hand', store=False, search='_search_on_hand')
# Inventory Fields
inventory_quantity = fields.Float(
'Counted Quantity', digits='Product Unit of Measure', groups='stock.group_stock_manager',
help="The product's counted quantity.")
inventory_diff_quantity = fields.Float(
'Difference', compute='_compute_inventory_diff_quantity', store=True,
help="Indicates the gap between the product's theoretical quantity and its counted quantity.",
readonly=True, digits='Product Unit of Measure', groups='stock.group_stock_manager')
inventory_date = fields.Date(
'Scheduled Date', compute='_compute_inventory_date', store=True, readonly=False,
help="Next date the On Hand Quantity should be counted.")
user_id = fields.Many2one(
'res.users', 'Assigned To', groups='stock.group_stock_manager', help="User assigned to do product count.")
@api.depends('quantity', 'reserved_quantity')
def _compute_available_quantity(self):
for quant in self:
quant.available_quantity = quant.quantity - quant.reserved_quantity
@api.depends('quantity')
def _compute_inventory_quantity(self):
if not self._is_inventory_mode():
self.inventory_quantity = 0
return
for quant in self:
quant.inventory_quantity = quant.quantity
@api.depends('location_id')
def _compute_inventory_date(self):
quants = self.filtered(lambda q: not q.inventory_date and q.location_id.usage in ['internal', 'transit'])
date_by_location = {loc: loc._get_next_inventory_date() for loc in quants.location_id}
for quant in quants:
quant.inventory_date = date_by_location[quant.location_id]
def _set_inventory_quantity(self):
""" Inverse method to create stock move when `inventory_quantity` is set
(`inventory_quantity` is only accessible in inventory mode).
"""
if not self._is_inventory_mode():
return
@api.depends('inventory_quantity')
def _compute_inventory_diff_quantity(self):
for quant in self:
# Get the quantity to create a move for.
rounding = quant.product_id.uom_id.rounding
diff = float_round(quant.inventory_quantity - quant.quantity, precision_rounding=rounding)
diff_float_compared = float_compare(diff, 0, precision_rounding=rounding)
# Create and vaidate a move so that the quant matches its `inventory_quantity`.
if diff_float_compared == 0:
continue
elif diff_float_compared > 0:
move_vals = quant._get_inventory_move_values(diff, quant.product_id.with_company(quant.company_id).property_stock_inventory, quant.location_id)
else:
move_vals = quant._get_inventory_move_values(-diff, quant.location_id, quant.product_id.with_company(quant.company_id).property_stock_inventory, out=True)
move = quant.env['stock.move'].with_context(inventory_mode=False).create(move_vals)
move._action_done()
quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity
def _search_on_hand(self, operator, value):
"""Handle the "on_hand" filter, indirectly calling `_get_domain_locations`."""
@@ -166,6 +160,9 @@ class StockQuant(models.Model):
quant = self.sudo().create(vals)
# Set the `inventory_quantity` field to create the necessary move.
quant.inventory_quantity = inventory_quantity
quant.user_id = vals.get('user_id', self.env.user.id)
quant.inventory_date = fields.Date.today()
return quant
res = super(StockQuant, self).create(vals)
if self._is_inventory_mode():
@@ -184,8 +181,8 @@ class StockQuant(models.Model):
fields.append('reserved_quantity')
result = super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
for group in result:
if self._is_inventory_mode():
group['inventory_quantity'] = group.get('quantity', 0)
if self.env.context.get('inventory_report_mode'):
group['inventory_quantity'] = False
if 'available_quantity' in fields:
group['available_quantity'] = group['quantity'] - group['reserved_quantity']
return result
@@ -200,7 +197,11 @@ class StockQuant(models.Model):
if any(field for field in vals.keys() if field not in allowed_fields):
raise UserError(_("Quant's editing is restricted, you can't do this operation."))
self = self.sudo()
return super(StockQuant, self).write(vals)
res = super(StockQuant, self).write(vals)
if res and self.env.context.get('inventory_report_mode'):
# update context to prevent recursive write call
self.with_context({'inventory_report_mode': False}).action_apply_inventory()
return res
return super(StockQuant, self).write(vals)
def action_view_stock_moves(self):
@@ -221,17 +222,116 @@ class StockQuant(models.Model):
@api.model
def action_view_quants(self):
self = self.with_context(search_default_internal_loc=1)
if not self.user_has_groups('stock.group_stock_multi_locations'):
company_user = self.env.company
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
if warehouse:
self = self.with_context(default_location_id=warehouse.lot_stock_id.id)
# If user have rights to write on quant, we set quants in inventory mode.
if self.user_has_groups('stock.group_stock_manager'):
self = self.with_context(inventory_mode=True)
self = self._set_view_context()
return self._get_quants_action(extend=True)
@api.model
def action_view_inventory(self):
""" Similar to _get_quants_action except specific for inventory adjustments (i.e. inventory counts). """
self = self._set_view_context()
self._quant_tasks()
ctx = dict(self.env.context or {})
ctx['no_at_date'] = True
action = {
'name': _('Stock On Hand'),
'view_mode': 'list',
'view_id': self.env.ref('stock.view_stock_quant_tree_inventory_editable').id,
'res_model': 'stock.quant',
'type': 'ir.actions.act_window',
'context': ctx,
'domain': [('location_id.usage', 'in', ['internal', 'transit'])],
'help': """
<p class="o_view_nocontent_barcode_scanner">
Want to speed up your inventory counts? Try our Barcode app
</p><p>
Barcode scanner can be activated via inventory settings.
Manual inventory adjustments can also be performed and pre-filled with
suggested counted quantity.
</p>
"""
}
return action
def action_apply_inventory(self):
quants_outdated = []
products_tracked_without_lot = []
for quant in self:
rounding = quant.product_uom_id.rounding
if fields.Float.is_zero(quant.inventory_diff_quantity, precision_rounding=rounding)\
and fields.Float.is_zero(quant.inventory_quantity, precision_rounding=rounding)\
and fields.Float.is_zero(quant.quantity, precision_rounding=rounding):
continue
if fields.Float.compare(quant.inventory_quantity - quant.inventory_diff_quantity, quant.quantity, precision_rounding=rounding):
quants_outdated.append(quant.id)
if quant.product_id.tracking in ['lot', 'serial'] and\
not quant.lot_id and quant.inventory_quantity != quant.quantity:
products_tracked_without_lot.append(quant.product_id.id)
# for some reason if multi-record, env.context doesn't pass to wizards...
ctx = dict(self.env.context or {})
ctx['default_quant_ids'] = self.ids
if quants_outdated:
ctx['default_quant_to_fix_ids'] = quants_outdated
return {
'name': _('Conflict in Inventory Adjustment'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'views': [(False, 'form')],
'res_model': 'stock.inventory.conflict',
'target': 'new',
'context': ctx,
}
if products_tracked_without_lot:
ctx['default_product_ids'] = products_tracked_without_lot
return {
'name': _('Tracked Products in Inventory Adjustment'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'views': [(False, 'form')],
'res_model': 'stock.track.confirmation',
'target': 'new',
'context': ctx,
}
self._apply_inventory()
def action_inventory_history(self):
self.ensure_one()
action = {
'name': _('History'),
'view_mode': 'list,form',
'res_model': 'stock.move.line',
'views': [(self.env.ref('stock.view_move_line_tree').id, 'list'), (False, 'form')],
'type': 'ir.actions.act_window',
'context': {
'search_default_inventory': 1,
'search_default_done': 1,
},
'domain': [
('product_id', '=', self.product_id.id),
('company_id', '=', self.company_id.id),
'|',
('location_id', '=', self.location_id.id),
('location_dest_id', '=', self.location_id.id),
],
}
if self.lot_id:
action['context']['search_default_lot_id'] = self.lot_id.id
if self.package_id:
action['context']['search_default_package_id'] = self.package_id.id
action['context']['search_default_result_package_id'] = self.package_id.id
if self.owner_id:
action['context']['search_default_owner_id'] = self.owner_id.id
return action
def action_set_inventory_quantity(self):
for quant in self:
quant.inventory_quantity = quant.quantity
self.user_id = self.env.user.id
def action_set_inventory_quantity_to_zero(self):
self.inventory_quantity = 0
self.inventory_diff_quantity = 0
@api.constrains('product_id')
def check_product_id(self):
if any(elem.product_id.type != 'product' for elem in self):
@@ -341,18 +441,16 @@ class StockQuant(models.Model):
if self.tracking == 'none' or self.product_id != self.lot_id.product_id:
vals['lot_id'] = None
quants = self._gather(self.product_id, self.location_id, lot_id=self.lot_id, package_id=self.package_id, owner_id=self.owner_id, strict=True)
reserved_quantity = sum(quants.mapped('reserved_quantity'))
quantity = sum(quants.mapped('quantity'))
quant = self._gather(
self.product_id, self.location_id, lot_id=self.lot_id,
package_id=self.package_id, owner_id=self.owner_id, strict=True)
if quant:
self.quantity = quant.quantity
vals['reserved_quantity'] = reserved_quantity
# Update `quantity` only if the user manually updated `inventory_quantity`.
if float_compare(self.quantity, self.inventory_quantity, precision_rounding=self.product_uom_id.rounding) == 0:
vals['quantity'] = quantity
# Special case: directly set the quantity to one for serial numbers,
# it'll trigger `inventory_quantity` compute.
if self.lot_id and self.tracking == 'serial':
vals['quantity'] = 1
vals['inventory_quantity'] = 1
if vals:
self.update(vals)
@@ -378,6 +476,30 @@ class StockQuant(models.Model):
if message:
return {'warning': {'title': _('Warning'), 'message': message}}
def _apply_inventory(self):
move_vals = []
for quant in self:
# Create and validate a move so that the quant matches its `inventory_quantity`.
if float_compare(quant.inventory_diff_quantity, 0, precision_rounding=quant.product_uom_id.rounding) > 0:
move_vals.append(
quant._get_inventory_move_values(quant.inventory_diff_quantity,
quant.product_id.with_company(quant.company_id).property_stock_inventory,
quant.location_id))
else:
move_vals.append(
quant._get_inventory_move_values(-quant.inventory_diff_quantity,
quant.location_id,
quant.product_id.with_company(quant.company_id).property_stock_inventory,
out=True))
moves = self.env['stock.move'].with_context(inventory_mode=False).create(move_vals)
moves._action_done()
self.location_id.write({'last_inventory_date': fields.Date.today()})
date_by_location = {loc: loc._get_next_inventory_date() for loc in self.mapped('location_id')}
for quant in self:
quant.inventory_date = date_by_location[quant.location_id]
self.write({'inventory_quantity': 0, 'user_id': False})
self.write({'inventory_diff_quantity': 0})
@api.model
def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None):
""" Increase or decrease `reserved_quantity` of a set of quants for a given set of
@@ -498,8 +620,10 @@ class StockQuant(models.Model):
"""
precision_digits = max(6, self.sudo().env.ref('product.decimal_product_uom').digits * 2)
# Use a select instead of ORM search for UoM robustness.
query = """SELECT id FROM stock_quant WHERE (round(quantity::numeric, %s) = 0 OR quantity IS NULL) AND round(reserved_quantity::numeric, %s) = 0;"""
params = (precision_digits, precision_digits)
query = """SELECT id FROM stock_quant WHERE (round(quantity::numeric, %s) = 0 OR quantity IS NULL)
AND round(reserved_quantity::numeric, %s) = 0
AND (round(inventory_quantity::numeric, %s) = 0 OR inventory_quantity IS NULL);"""
params = (precision_digits, precision_digits, precision_digits)
self.env.cr.execute(query, params)
quant_ids = self.env['stock.quant'].browse([quant['id'] for quant in self.env.cr.dictfetchall()])
quant_ids.sudo().unlink()
@@ -516,6 +640,7 @@ class StockQuant(models.Model):
SELECT min(id) as to_update_quant_id,
(array_agg(id ORDER BY id))[2:array_length(array_agg(id), 1)] as to_delete_quant_ids,
SUM(reserved_quantity) as reserved_quantity,
SUM(inventory_quantity) as inventory_quantity,
SUM(quantity) as quantity,
MIN(in_date) as in_date
FROM stock_quant
@@ -526,6 +651,7 @@ class StockQuant(models.Model):
UPDATE stock_quant q
SET quantity = d.quantity,
reserved_quantity = d.reserved_quantity,
inventory_quantity = d.inventory_quantity,
in_date = d.in_date
FROM dupes d
WHERE d.to_update_quant_id = q.id
@@ -550,19 +676,19 @@ class StockQuant(models.Model):
"inventory session", meaning a mode where we need to create the stock.move
record necessary to be consistent with the `inventory_quantity` field.
"""
return self.env.context.get('inventory_mode') is True and self.user_has_groups('stock.group_stock_manager')
return self.env.context.get('inventory_mode') and self.user_has_groups('stock.group_stock_manager')
@api.model
def _get_inventory_fields_create(self):
""" Returns a list of fields user can edit when he want to create a quant in `inventory_mode`.
"""
return ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'inventory_quantity']
return ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id'] + self._get_inventory_fields_write()
@api.model
def _get_inventory_fields_write(self):
""" Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`.
"""
return ['inventory_quantity']
return ['inventory_quantity', 'inventory_diff_quantity', 'inventory_date', 'user_id']
def _get_inventory_move_values(self, qty, location_id, location_dest_id, out=False):
""" Called when user manually set a new quantity (via `inventory_quantity`)
@@ -574,8 +700,12 @@ class StockQuant(models.Model):
:return: dict with all values needed to create a new `stock.move` with its move line.
"""
self.ensure_one()
if fields.Float.is_zero(qty, 0, precision_rounding=self.product_uom_id.rounding):
name = _('Product Quantity Confirmed')
else:
name = _('Product Quantity Updated')
return {
'name': _('Product Quantity Updated'),
'name': name,
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': qty,
@@ -583,6 +713,7 @@ class StockQuant(models.Model):
'state': 'confirmed',
'location_id': location_id.id,
'location_dest_id': location_dest_id.id,
'is_inventory': True,
'move_line_ids': [(0, 0, {
'product_id': self.product_id.id,
'product_uom_id': self.product_uom_id.id,
@@ -597,9 +728,22 @@ class StockQuant(models.Model):
})]
}
def _set_view_context(self):
""" Adds context when opening quants related views. """
if not self.user_has_groups('stock.group_stock_multi_locations'):
company_user = self.env.company
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
if warehouse:
self = self.with_context(default_location_id=warehouse.lot_stock_id.id, hide_location=True)
# If user have rights to write on quant, we set quants in inventory mode.
if self.user_has_groups('stock.group_stock_manager'):
self = self.with_context(inventory_mode=True)
return self
@api.model
def _get_quants_action(self, domain=None, extend=False):
""" Returns an action to open quant view.
""" Returns an action to open (non-inventory adjustment) quant view.
Depending of the context (user have right to be inventory mode or not),
the list view will be editable or readonly.
@@ -608,6 +752,7 @@ class StockQuant(models.Model):
"""
self._quant_tasks()
ctx = dict(self.env.context or {})
ctx['inventory_report_mode'] = True
ctx.pop('group_by', None)
action = {
'name': _('Stock On Hand'),
-6
View File
@@ -517,12 +517,6 @@ class ProcurementGroup(models.Model):
if use_new_cursor:
self._cr.commit()
# Run cyclic inventories
self.env['stock.inventory']._run_inventory_tasks(company_id)
if use_new_cursor:
self._cr.commit()
@api.model
def run_scheduler(self, use_new_cursor=False, company_id=False):
""" Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules
-133
View File
@@ -303,139 +303,6 @@ class StockWarehouseOrderpoint(models.Model):
]
class Inventory(models.Model):
_inherit = 'stock.inventory'
_populate_sizes = {'small': 5, 'medium': 10, 'large': 20}
_populate_dependencies = ['stock.location', 'product.product']
def _populate(self, size):
inventories = super()._populate(size)
def start_inventory_sample(sample_ratio):
random = populate.Random('start_inventory_sample')
inventories_to_start = self.browse(random.sample(inventories.ids, int(len(inventories.ids) * sample_ratio)))
# Start empty to let the stock.inventory.line populate create lines
inventories_to_start.start_empty = True
for inventory in inventories_to_start:
inventory.action_start()
# Start 80 % of adjustment inventory
start_inventory_sample(0.8)
return inventories
def _populate_factories(self):
company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK]
valid_locations = self.env['stock.location'].search([
('company_id', 'in', company_ids),
('usage', 'in', ['internal', 'transit'])
])
locations_by_company = dict(groupby(valid_locations, key=lambda loc: loc.company_id.id))
locations_by_company = {company_id: self.env['stock.location'].concat(*locations) for company_id, locations in locations_by_company.items()}
products = self.env['product.product'].browse(self.env.registry.populated_models['product.product']).filtered(lambda p: p.type == 'product')
def get_locations_ids(values, counter, random):
location_ids_company = locations_by_company[values['company_id']]
# We set locations to ease the creation of stock.inventory.line
# Should be larger enough to avoid the emptiness of generator_product_loc_dict
return random.sample(location_ids_company.ids, int(len(location_ids_company.ids) * 0.5))
def get_product_ids(values, counter, random):
# We set products to ease the creation of stock.inventory.line
# Should be larger enough to avoid the emptiness of generator_product_loc_dict
return random.sample(products.ids, int(len(products.ids) * 0.5))
return [
('name', populate.constant("Inventory-Pop-{counter}")),
('company_id', populate.iterate(company_ids)),
('location_ids', populate.compute(get_locations_ids)),
('product_ids', populate.compute(get_product_ids)),
]
class InventoryLine(models.Model):
_inherit = 'stock.inventory.line'
_populate_sizes = {'small': 500, 'medium': 5_000, 'large': 20_000}
_populate_dependencies = ['stock.inventory']
def _populate(self, size):
inventory_lines = super()._populate(size)
def create_missing_lots():
_logger.info("Create lot/serial for inventory line to be ready to validate")
lots_values = []
for line in inventory_lines:
if line.product_tracking == 'none':
continue
lots_values.append({
'name': "InLine-%d" % line.id,
'product_id': line.product_id.id,
'company_id': line.inventory_id.company_id.id
})
lots = self.env['stock.production.lot'].create(lots_values).ids[::-1]
for line in inventory_lines:
if line.product_tracking == 'none':
continue
line.prod_lot_id = lots.pop()
def validate_inventory_sample(sample_ratio):
# Validate the inventory adjustment, useful to have a stock at beginning
inventories_ids = inventory_lines.inventory_id.ids
random = populate.Random('validate_inventory')
inventories_to_done = self.env['stock.inventory'].browse(random.sample(inventories_ids, int(len(inventories_ids) * sample_ratio)))
_logger.info("Validate %d inventory adjustment" % len(inventories_to_done))
for inventory in inventories_to_done:
inventory.action_validate()
# Create missing tracking lot/serial in batch
create_missing_lots()
# (Un)comment to test a DB with a current stock.
# validate_inventory_sample(0.8)
return inventory_lines
def _populate_factories(self):
inventories = self.env['stock.inventory'].browse(self.env.registry.populated_models['stock.inventory'])
inventories = inventories.filtered(lambda i: i.state == 'confirm')
def compute_product_location(iterator, field_name, model_name):
random = populate.Random('compute_product_location')
# To avoid create twice the same line inventory (avoid _check_no_duplicate_line) : product/location (limitation for lot)
# Use generator to avoid cartisian product in memory
generator_product_loc_dict = {}
for inventory in inventories:
# TODO: randomize cartesian product
generator_product_loc_dict[inventory.id] = cartesian_product(
random.sample(inventory.location_ids, len(inventory.location_ids)),
random.sample(inventory.product_ids, len(inventory.product_ids))
)
for values in iterator:
loc_id, product = next(generator_product_loc_dict[values['inventory_id']])
values['product_id'] = product.id
values['location_id'] = loc_id.id
yield values
def get_product_qty(values, counter, random):
product = self.env['product.product'].browse(values['product_id'])
if product.tracking == 'serial':
return 1.0
else:
return random.randint(5, 25)
return [
('inventory_id', populate.iterate(inventories.ids)),
('_compute_product_location', compute_product_location),
('product_qty', populate.compute(get_product_qty)),
]
class PickingType(models.Model):
_inherit = 'stock.picking.type'
+44 -47
View File
@@ -3,54 +3,51 @@
<data>
<template id="report_inventory">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<br/>
<h2 t-field="o.name"/>
<div class="row mt32 mb32" id="informations">
<div t-if="o.date" class="col-auto mw-100 mb-2">
<strong>Date:</strong>
<p class="m-0" t-field="o.date"/>
</div>
</div>
<t t-set="locations" t-value="o.line_ids.mapped('location_id')"/>
<table class="table table-sm">
<thead>
<tr>
<th groups="stock.group_stock_multi_locations"><strong>Location</strong></th>
<th><strong>Product</strong></th>
<th groups="stock.group_production_lot"><strong>Lot/Serial Number</strong></th>
<th groups="stock.group_tracking_lot"><strong>Package</strong></th>
<th class="text-right"><strong>On Hand Quantity</strong></th>
<th class="text-right"><strong>Counted Quantity</strong></th>
<t t-call="web.external_layout">
<div class="page">
<br/>
<h2>Inventory Count Sheet</h2>
<t t-set="locations" t-value="docs.mapped('location_id')"/>
<table class="table table-sm">
<thead>
<tr>
<th groups="stock.group_stock_multi_locations"><strong>Location</strong></th>
<th><strong>Product</strong></th>
<th groups="stock.group_production_lot"><strong>Lot/Serial Number</strong></th>
<th groups="stock.group_tracking_lot"><strong>Package</strong></th>
<th class="text-right"><strong>Available Quantity</strong></th>
<th class="text-right"><strong>On hand Quantity</strong></th>
<th class="text-right"><strong>Counted Quantity</strong></th>
</tr>
</thead>
<tbody>
<t t-foreach="locations" t-as="location">
<tr groups="stock.group_stock_multi_locations">
<td colspan="2"><strong t-esc="location.display_name"/></td>
<td groups="stock.group_production_lot"></td>
<td groups="stock.group_tracking_lot"></td>
<td></td>
</tr>
</thead>
<tbody>
<t t-foreach="locations" t-as="location">
<tr groups="stock.group_stock_multi_locations">
<td colspan="2"><strong t-esc="location.display_name"/></td>
<td groups="stock.group_production_lot"></td>
<td groups="stock.group_tracking_lot"></td>
<td></td>
</tr>
<tr t-foreach="o.line_ids.filtered(lambda line: line.location_id.id == location.id)" t-as="line">
<td groups="stock.group_stock_multi_locations"></td>
<td><span t-field="line.product_id"/></td>
<td groups="stock.group_production_lot"><span t-field="line.prod_lot_id"/></td>
<td groups="stock.group_tracking_lot"><span t-field="line.package_id"/></td>
<td class="text-right"><span t-field="line.theoretical_qty"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
<td class="text-right"><span t-field="line.product_qty"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</t>
</t>
<tr t-foreach="docs.filtered(lambda quant: quant.location_id.id == location.id)" t-as="line">
<td groups="stock.group_stock_multi_locations"></td>
<td><span t-field="line.product_id"/></td>
<td groups="stock.group_production_lot"><span t-field="line.lot_id"/></td>
<td groups="stock.group_tracking_lot"><span t-field="line.package_id"/></td>
<td class="text-right"><span t-field="line.available_quantity"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
<td class="text-right"><span t-field="line.quantity"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
<td class="text-right">
<!-- If 0, then leave blank so users have space to write a number -->
<t t-if="line.inventory_quantity == 0"><span></span></t>
<t t-else=""><span t-field="line.inventory_quantity"/></t>
<span t-field="line.product_uom_id" groups="uom.group_uom"/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</t>
</template>
</data>
</odoo>
+3 -3
View File
@@ -23,12 +23,12 @@
</record>
<record id="action_report_inventory" model="ir.actions.report">
<field name="name">Count Sheet</field>
<field name="model">stock.inventory</field>
<field name="model">stock.quant</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">stock.report_inventory</field>
<field name="report_file">stock.report_inventory</field>
<field name="print_report_name">'Inventory - %s' % (object.name)</field>
<field name="binding_model_id" ref="model_stock_inventory"/>
<field name="print_report_name">'Count Sheet'</field>
<field name="binding_model_id" ref="model_stock_quant"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_quant_package_barcode" model="ir.actions.report">
+4 -4
View File
@@ -89,10 +89,10 @@ class MrpStockReport(models.TransientModel):
res_model = 'stock.picking'
res_id = picking_id.id
ref = picking_id.name
elif move_line.move_id.inventory_id:
res_model = 'stock.inventory'
res_id = move_line.move_id.inventory_id.id
ref = 'Inv. Adj.: ' + move_line.move_id.inventory_id.name
elif move_line.move_id.is_inventory:
res_model = 'stock.move'
res_id = move_line.move_id.id
ref = 'Inventory Adjustment'
elif move_line.move_id.scrapped and move_line.move_id.scrap_ids:
res_model = 'stock.scrap'
res_id = move_line.move_id.scrap_ids[0].id
+2 -4
View File
@@ -13,10 +13,6 @@ access_stock_picking_type_manager,stock.picking.type manager,model_stock_picking
access_stock_production_lot_user,stock.production.lot user,model_stock_production_lot,stock.group_stock_user,1,1,1,1
access_stock_move_manager,stock.move manager,model_stock_move,stock.group_stock_manager,1,1,1,1
access_stock_move_user,stock.move user,model_stock_move,stock.group_stock_user,1,1,1,0
access_stock_inventory_user,stock.inventory user,model_stock_inventory,stock.group_stock_user,1,1,1,0
access_stock_inventory_manager,stock.inventory manager,model_stock_inventory,stock.group_stock_manager,1,1,1,1
access_stock_inventory_line_user,stock.inventory.line user,model_stock_inventory_line,stock.group_stock_user,1,1,1,0
access_stock_inventory_line_manager,stock.inventory.line manager,model_stock_inventory_line,stock.group_stock_manager,1,1,1,1
access_product_product_stock_user,product_product_stock_user,product.model_product_product,stock.group_stock_user,1,0,0,0
access_product_template_stock_user,product.template stock user,product.model_product_template,stock.group_stock_user,1,0,0,0
access_uom_category_stock_manager,uom.category stock_manager,uom.model_uom_category,stock.group_stock_manager,1,1,1,1
@@ -87,3 +83,5 @@ access_stock_storage_category_user,stock.storage.category.user,model_stock_stora
access_stock_storage_category_manager,stock.storage.category.manager,model_stock_storage_category,stock.group_stock_manager,1,1,1,1
access_stock_storage_category_capacity_user,stock.storage.category.capacity.user,model_stock_storage_category_capacity,base.group_user,1,0,0,0
access_stock_storage_category_capacity_manager,stock.storage.category.capacity.manager,model_stock_storage_category_capacity,stock.group_stock_manager,1,1,1,1
access_stock_inventory_conflict,stock.inventory.conflict,model_stock_inventory_conflict,stock.group_stock_manager,1,1,1,0
access_stock_request_count,stock.request.count,model_stock_request_count,stock.group_stock_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
13 access_stock_production_lot_user stock.production.lot user model_stock_production_lot stock.group_stock_user 1 1 1 1
14 access_stock_move_manager stock.move manager model_stock_move stock.group_stock_manager 1 1 1 1
15 access_stock_move_user stock.move user model_stock_move stock.group_stock_user 1 1 1 0
access_stock_inventory_user stock.inventory user model_stock_inventory stock.group_stock_user 1 1 1 0
access_stock_inventory_manager stock.inventory manager model_stock_inventory stock.group_stock_manager 1 1 1 1
access_stock_inventory_line_user stock.inventory.line user model_stock_inventory_line stock.group_stock_user 1 1 1 0
access_stock_inventory_line_manager stock.inventory.line manager model_stock_inventory_line stock.group_stock_manager 1 1 1 1
16 access_product_product_stock_user product_product_stock_user product.model_product_product stock.group_stock_user 1 0 0 0
17 access_product_template_stock_user product.template stock user product.model_product_template stock.group_stock_user 1 0 0 0
18 access_uom_category_stock_manager uom.category stock_manager uom.model_uom_category stock.group_stock_manager 1 1 1 1
83 access_stock_storage_category_manager stock.storage.category.manager model_stock_storage_category stock.group_stock_manager 1 1 1 1
84 access_stock_storage_category_capacity_user stock.storage.category.capacity.user model_stock_storage_category_capacity base.group_user 1 0 0 0
85 access_stock_storage_category_capacity_manager stock.storage.category.capacity.manager model_stock_storage_category_capacity stock.group_stock_manager 1 1 1 1
86 access_stock_inventory_conflict stock.inventory.conflict model_stock_inventory_conflict stock.group_stock_manager 1 1 1 0
87 access_stock_request_count stock.request.count model_stock_request_count stock.group_stock_manager 1 1 1 0
-12
View File
@@ -129,18 +129,6 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="stock_inventory_line_comp_rule">
<field name="name">Inventory Line multi-company</field>
<field name="model_id" ref="model_stock_inventory_line"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="stock_inventory_comp_rule">
<field name="name">Inventory multi-company</field>
<field name="model_id" ref="model_stock_inventory"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="stock_warehouse_orderpoint_rule">
<field name="name">stock_warehouse.orderpoint multi-company</field>
<field name="model_id" search="[('model','=','stock.warehouse.orderpoint')]" model="ir.model"/>
@@ -1,13 +1,10 @@
odoo.define('stock.InventoryReportListController', function (require) {
"use strict";
var core = require('web.core');
var ListController = require('web.ListController');
var qweb = core.qweb;
var InventoryReportListController = ListController.extend({
buttons_template: 'StockInventoryReport.Buttons',
// -------------------------------------------------------------------------
// Public
@@ -24,11 +21,9 @@ var InventoryReportListController = ListController.extend({
renderButtons: function ($node) {
this._super.apply(this, arguments);
if (this.context.no_at_date) {
return;
this.$buttons.find('button.o_button_at_date').hide();
}
var $buttonToDate = $(qweb.render('InventoryReport.Buttons'));
$buttonToDate.on('click', this._onOpenWizard.bind(this));
this.$buttons.prepend($buttonToDate);
this.$buttons.on('click', '.o_button_at_date', this._onOpenWizard.bind(this));
},
// -------------------------------------------------------------------------
@@ -7,20 +7,43 @@ var InventoryReportListController = require('stock.InventoryReportListController
var _t = core._t;
/**
* The purpose of this override is to avoid to have two or more similar records
* in the list view.
* The purpose of this override has 2 purposes:
*
* It's used in quant list view, a list editable where when you create a new
* line about a quant who already exists, we want to update the existing one
* instead of create a new one, and then we don't want to have two similar line
* in the list view, so we refresh it.
* 1. Avoid having two or more similar records in the list view.
* It is expected to be used in quant list views, when the list is editable
* and allows new line creation. Checks for whether newly created quant
* already exists and if it does => update the existing quant instead of the
* newly created one, and then we refresh the view to avoid having 2 similar lines.
*
* 2. Support Inventory Adjustments (i.e. inventory counts) view.
* Specifically adds necessary buttons (or hides them when appropriate) and
* autosaving logic to prevent button actions before edits are saved.
*/
var SingletonListController = InventoryReportListController.extend({
buttons_template: 'StockQuant.Buttons',
// -------------------------------------------------------------------------
// Public
// -------------------------------------------------------------------------
/**
* @override
* @return {Promise} rejected when update the list because we don't want
* anymore to select a cell who maybe doesn't exist anymore.
*/
renderButtons: function ($node) {
this._super.apply(this, arguments);
this.$buttons.on('click', '.o_button_apply_inventory', this._onApplyInventory.bind(this));
this.$buttons.on('click', '.o_button_request_count_inventory', this._onRequestCountInventory.bind(this));
this.$buttons.on('click', '.o_button_set_inventory', this._onSetInventory.bind(this));
},
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
/**
* @override
* @return {Promise} rejected when updating the list because we don't want
* to select a cell that might not exist anymore.
*/
_confirmSave: function (id) {
var newRecord = this.model.localData[id];
@@ -51,8 +74,8 @@ var SingletonListController = InventoryReportListController.extend({
var similarRecords = findSimilarRecords(handle);
if (similarRecords.length > 1) {
var notification = _t("You tried to create a record who already exists."+
"<br/>This last one has been modified instead.");
var notification = _t("You tried to create a record that already exists."+
"<br/>The existing record has been modified instead.");
this.do_notify(_t("This record already exists."), notification);
this.reload();
return Promise.reject();
@@ -61,6 +84,103 @@ var SingletonListController = InventoryReportListController.extend({
return this._super.apply(this, arguments);
}
},
/**
* Override to show/hide inventory adjustment specific buttons.
* Maybe a bit too hacky since it's not the intended purpose of this function,
* but for some reason `_updateControlPanel` isn't called during reload action
* after triggering any of these buttons.
*
* @override
*/
_renderHeaderButtons() {
this._super.apply(this, arguments);
if (this.selectedRecords.length > 0 && !this.context.inventory_report_mode) {
$('.o_button_apply_inventory').removeClass('d-none');
$('.o_button_request_count_inventory').removeClass('d-none');
$('.o_button_set_inventory').removeClass('d-none');
} else {
$('.o_button_apply_inventory').addClass('d-none');
$('.o_button_request_count_inventory').addClass('d-none');
$('.o_button_set_inventory').addClass('d-none');
}
},
// -------------------------------------------------------------------------
// Handlers
// -------------------------------------------------------------------------
_onApplyInventory: function (ev) {
var self = this;
var ids = this.getSelectedIds();
if (!ids.length) {
var modified_records = this.initialState.data.filter(record => record.data.inventory_diff_quantity != 0)
ids = modified_records.map(record => record.data.id)
}
if (ids.length) {
return this._rpc({
model: 'stock.quant',
method: 'action_apply_inventory',
args: [ids],
context: this.context,
}).then((result) => {
if (!result) {
return self.trigger_up('reload');
}
return self.do_action(result, {on_close: () => this.reload(),});
})
}
},
/**
*
* @private
* @param {OdooEvent} ev
*/
_onButtonClicked: function (ev) {
ev.stopPropagation();
var self = this;
return self.saveRecord(ev.data.record.id, {
stayInEdit: true,
}).then(function () {
// we need to re-get the record to make sure we have changes made
// by the basic model, such as the new res_id, if the record is
// new.
var record = self.model.get(ev.data.record.id);
return self._callButtonAction(ev.data.attrs, record);
}).then(function () {
self._enableButtons();
}).guardedCatch(this._enableButtons.bind(this));
},
_onRequestCountInventory: function (ev) {
var ids = this.getSelectedIds();
if ( ids.length ) {
return this.do_action('stock.action_stock_request_count',
{
additional_context: {
default_quant_ids: ids,
},
on_close: () => this.reload(),
});
}
},
_onSetInventory: function (ev) {
var self = this;
var ids = this.getSelectedIds();
if (ids.length) {
return this._rpc({
model: 'stock.quant',
method: 'action_set_inventory_quantity',
args: [ids],
context: this.context,
}).then(() => {
return self.trigger_up('reload');
});
}
},
});
return SingletonListController;
@@ -0,0 +1,34 @@
odoo.define('stock.SingletonListRenderer', function (require) {
"use strict";
var core = require('web.core');
var ListRenderer = require('web.ListRenderer');
var _t = core._t;
/**
* The purpose of this override is to disable line buttons when the row is edited.
*
*/
var SingletonListRenderer = ListRenderer.extend({
_disableRecordSelectors: function () {
this._super.apply(this, arguments);
var row = this._getRow(this._getRecordID(this.currentRow))[0];
var lineButtons = row.querySelectorAll('button[name=action_set_inventory_quantity]');
lineButtons.forEach(elem => elem.setAttribute('disabled', true));
},
/**
* @private
*/
_enableRecordSelectors: function () {
this._super.apply(this, arguments);
var lineButtons = this.el.querySelectorAll('button.disabled');
lineButtons.forEach(elem => elem.removeAttribute('disabled'));
},
});
return SingletonListRenderer;
});
@@ -3,11 +3,13 @@ odoo.define('stock.SingletonListView', function (require) {
var InventoryReportListView = require('stock.InventoryReportListView');
var SingletonListController = require('stock.SingletonListController');
var SingletonListRenderer = require('stock.SingletonListRenderer');
var viewRegistry = require('web.view_registry');
var SingletonListView = InventoryReportListView.extend({
config: _.extend({}, InventoryReportListView.prototype.config, {
Controller: SingletonListController,
Renderer: SingletonListRenderer,
}),
});
@@ -1,89 +0,0 @@
odoo.define('stock.InventoryValidationController', function (require) {
"use strict";
var core = require('web.core');
var ListController = require('web.ListController');
var _t = core._t;
var qweb = core.qweb;
var InventoryValidationController = ListController.extend({
events: _.extend({
'click .o_button_validate_inventory': '_onValidateInventory'
}, ListController.prototype.events),
/**
* @override
*/
init: function (parent, model, renderer, params) {
var context = renderer.state.getContext();
this.inventory_id = context.active_id;
return this._super.apply(this, arguments);
},
// -------------------------------------------------------------------------
// Public
// -------------------------------------------------------------------------
/**
* @override
*/
renderButtons: function () {
this._super.apply(this, arguments);
var $validationButton = $(qweb.render('InventoryLines.Buttons'));
this.$buttons.prepend($validationButton);
},
// -------------------------------------------------------------------------
// Handlers
// -------------------------------------------------------------------------
/**
* Handler called when user click on validation button in inventory lines
* view. Makes an rpc to try to validate the inventory, then will go back on
* the inventory view form if it was validated.
* This method could also open a wizard in case something was missing.
*
* @private
*/
_onValidateInventory: function () {
var self = this;
var prom = Promise.resolve();
var recordID = this.renderer.getEditableRecordID();
if (recordID) {
// If user's editing a record, we wait to save it before to try to
// validate the inventory.
prom = this.saveRecord(recordID);
}
prom.then(function () {
self._rpc({
model: 'stock.inventory',
method: 'action_validate',
args: [self.inventory_id]
}).then(function (res) {
var exitCallback = function (infos) {
// In case we discarded a wizard, we do nothing to stay on
// the same view...
if (infos && infos.special) {
return;
}
// ... but in any other cases, we go back on the inventory form.
self.do_notify(
false,
_t("The inventory has been validated"));
self.trigger_up('history_back');
};
if (_.isObject(res)) {
self.do_action(res, { on_close: exitCallback });
} else {
return exitCallback();
}
});
});
},
});
return InventoryValidationController;
});
@@ -1,16 +0,0 @@
odoo.define('stock.InventoryValidationView', function (require) {
"use strict";
var InventoryValidationController = require('stock.InventoryValidationController');
var ListView = require('web.ListView');
var viewRegistry = require('web.view_registry');
var InventoryValidationView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: InventoryValidationController
})
});
viewRegistry.add('inventory_validate_button', InventoryValidationView);
});
@@ -1,8 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<button t-name="InventoryReport.Buttons" class="btn btn-primary" type="button">
Inventory at Date
</button>
<t t-extend="ListView.buttons" t-name="StockInventoryReport.Buttons">
<t t-jquery="button.o_list_button_discard" t-operation="after">
<button class="btn btn-primary o_button_at_date" type="button">
Inventory at Date
</button>
</t>
</t>
<t t-extend="StockInventoryReport.Buttons" t-name="StockQuant.Buttons">
<t t-jquery="button.o_button_at_date" t-operation="after">
<button class="btn btn-primary d-none o_button_apply_inventory" type="button">
Apply
</button>
<button class="btn btn-primary d-none o_button_set_inventory" type="button">
Set
</button>
<button class="btn btn-primary d-none o_button_request_count_inventory" type="button">
Request a Count
</button>
</t>
</t>
</templates>
-2
View File
@@ -16,8 +16,6 @@ class TestStockCommon(common.TransactionCase):
cls.StockQuantObj = cls.env['stock.quant']
cls.PickingObj = cls.env['stock.picking']
cls.MoveObj = cls.env['stock.move']
cls.InvObj = cls.env['stock.inventory']
cls.InvLineObj = cls.env['stock.inventory.line']
cls.LotObj = cls.env['stock.production.lot']
# Model Data
+1 -1
View File
@@ -53,5 +53,5 @@ class TestStockCommon(common.TestProductCommon):
})
# Existing data
cls.existing_inventories = cls.env['stock.inventory'].search([])
cls.existing_inventories = cls.env['stock.quant'].search([('inventory_quantity', '!=', 0.0)])
cls.existing_quants = cls.env['stock.quant'].search([])
File diff suppressed because it is too large Load Diff
+21 -39
View File
@@ -1146,14 +1146,12 @@ class TestSinglePicking(TestStockCommon):
self.assertEqual(len(move1.move_line_ids), 1)
self.assertEqual(move1.move_line_ids.product_qty, 1)
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
'location_ids': [(4, self.stock_location)],
'product_ids': [(4, self.productA.id)],
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'inventory_quantity': 2
})
inventory.action_start()
inventory.line_ids.product_qty = 2
inventory.action_validate()
inventory_quant.action_apply_inventory()
delivery_order.action_assign()
self.assertEqual(delivery_order.state, 'assigned')
self.assertEqual(move1.state, 'assigned')
@@ -1202,15 +1200,13 @@ class TestSinglePicking(TestStockCommon):
self.assertEqual(len(move1.move_line_ids), 1)
self.assertEqual(move1.move_line_ids.product_qty, 1)
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
'location_ids': [(4, self.stock_location)],
'product_ids': [(4, self.productA.id)],
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'inventory_quantity': 2,
'lot_id': lot1.id
})
inventory.action_start()
inventory.line_ids.prod_lot_id = lot1
inventory.line_ids.product_qty = 2
inventory.action_validate()
inventory_quant.action_apply_inventory()
delivery_order.action_assign()
self.assertEqual(delivery_order.state, 'assigned')
self.assertEqual(move1.state, 'assigned')
@@ -1262,20 +1258,13 @@ class TestSinglePicking(TestStockCommon):
self.assertEqual(len(move1.move_line_ids), 1)
self.assertEqual(move1.move_line_ids.product_qty, 1)
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
'location_ids': [(4, self.stock_location)],
'product_ids': [(4, self.productA.id)],
})
inventory.action_start()
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
'location_id': inventory.location_ids[0].id,
'prod_lot_id': lot2.id,
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'product_qty': 1,
'inventory_quantity': 1,
'lot_id': lot2.id
})
inventory.action_validate()
inventory_quant.action_apply_inventory()
delivery_order.action_assign()
self.assertEqual(delivery_order.state, 'assigned')
self.assertEqual(move1.state, 'assigned')
@@ -1329,20 +1318,13 @@ class TestSinglePicking(TestStockCommon):
self.assertEqual(len(move1.move_line_ids), 1)
self.assertEqual(move1.move_line_ids.product_qty, 1)
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
'location_ids': [(4, self.stock_location)],
'product_ids': [(4, self.productA.id)],
})
inventory.action_start()
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
'location_id': inventory.location_ids[0].id,
'prod_lot_id': serial2.id,
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'product_qty': 1,
'inventory_quantity': 1,
'lot_id': serial2.id
})
inventory.action_validate()
inventory_quant.action_apply_inventory()
delivery_order.action_assign()
self.assertEqual(delivery_order.state, 'assigned')
self.assertEqual(move1.state, 'assigned')
+23 -38
View File
@@ -27,7 +27,7 @@ class TestMultiCompany(TransactionCase):
'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
})
cls.user_b = cls.env['res.users'].create({
'name': 'user company a with access to company b',
'name': 'user company b with access to company a',
'login': 'user b',
'groups_id': [(6, 0, [group_user.id, group_stock_manager.id])],
'company_id': cls.company_b.id,
@@ -103,60 +103,45 @@ class TestMultiCompany(TransactionCase):
shared_partner.with_user(self.user_b).property_stock_customer = self.stock_location_a
def test_inventory_1(self):
"""Create an inventory in Company A for a product limited to Company A and, as a user of company
B, start the inventory and set its counted quantity to 10 before validating. The inventory
lines and stock moves should belong to Company A. The inventory loss location used should be
the one of Company A.
"""Create a quant (inventory adjustment) in Company A for a product limited to Company A and
as a user of company B, apply the inventory adjustment and set its counted quantity to 10
before validating. The quant and stock moves should belong to Company A.
"""
product = self.env['product.product'].create({
'type': 'product',
'company_id': self.company_a.id,
'name': 'Product limited to company A',
})
inventory = self.env['stock.inventory'].with_user(self.user_a).create({})
self.assertEqual(inventory.company_id, self.company_a)
inventory.with_user(self.user_b).action_start()
inventory.with_user(self.user_b).line_ids = [(0, 0, {
'product_qty': 10,
'product_id': product.id,
inventory_quant = self.env['stock.quant'].with_user(self.user_a).with_context(inventory_mode=True).create({
'location_id': self.stock_location_a.id,
})]
inventory.with_user(self.user_b).action_validate()
self.assertEqual(inventory.line_ids.company_id, self.company_a)
self.assertEqual(inventory.move_ids.company_id, self.company_a)
self.assertEqual(inventory.move_ids.location_id.company_id, self.company_a)
'product_id': product.id,
'inventory_quantity': 0
})
self.assertEqual(inventory_quant.company_id, self.company_a)
inventory_quant.with_user(self.user_b).inventory_quantity = 10
inventory_quant.with_user(self.user_b).action_apply_inventory()
last_move_id = self.env['stock.move'].search([('is_inventory', '=', True)])[-1]
self.assertEqual(inventory_quant.company_id, self.company_a)
self.assertEqual(last_move_id.company_id, self.company_a)
self.assertEqual(last_move_id.quantity_done, 10)
self.assertEqual(last_move_id.location_id.company_id, self.company_a)
def test_inventory_2(self):
"""Create an empty inventory in Company A and check it is not possible to use products limited
to Company B in it.
"""Try to create a quant (inventory adjustment) in Company A and check it is not possible to use
products limited to Company B in it.
"""
product = self.env['product.product'].create({
'name': 'product limited to company b',
'company_id': self.company_b.id,
'type': 'product'
})
inventory = self.env['stock.inventory'].with_user(self.user_a).create({})
inventory.with_user(self.user_a).action_start()
inventory.with_user(self.user_a).line_ids = [(0, 0, {
'product_id': product.id,
'product_qty': 10,
'location_id': self.stock_location_a.id,
})]
with self.assertRaises(UserError):
inventory.with_user(self.user_a).action_validate()
def test_inventory_3(self):
"""As a user of Company A, check it is not possible to start an inventory adjustment for
a product limited to Company B.
"""
product = self.env['product.product'].create({
'name': 'product limited to company b',
'company_id': self.company_b.id,
'type': 'product'
})
inventory = self.env['stock.inventory'].with_user(self.user_a).create({'product_ids': [(4, product.id)]})
with self.assertRaises(UserError):
inventory.with_user(self.user_a).action_start()
self.env['stock.quant'].with_user(self.user_a).with_context(inventory_mode=True).create({
'location_id': self.stock_location_a.id,
'product_id': product.id,
'inventory_quantity': 10
})
def test_picking_1(self):
"""As a user of Company A, create a picking and use a picking type of Company B, check the
@@ -63,7 +63,7 @@ class TestEditableQuant(TransactionCase):
'product_id': self.product.id,
'location_id': self.stock.id,
'inventory_quantity': 24
})
}).action_apply_inventory()
quants = self.env['stock.quant'].search([
('product_id', '=', self.product.id),
('quantity', '>', 0),
@@ -100,6 +100,7 @@ class TestEditableQuant(TransactionCase):
'location_id': self.room1.id,
'inventory_quantity': 24,
})
second_quant.action_apply_inventory()
quants = self.env['stock.quant'].search([
('product_id', '=', self.product.id),
('quantity', '>', 0),
@@ -115,10 +116,10 @@ class TestEditableQuant(TransactionCase):
self.assertEqual(len(stock_move), 1)
def test_create_quant_3(self):
""" Try to create a quant with `inventory_quantity` but not in inventory mode.
Creates two quants not in inventory mode:
""" Try to create a quant with `inventory_quantity` but without applying it.
Creates two quants:
- One with `quantity` (this one must be OK)
- One with `inventory_quantity` (this one must be null)
- One with `inventory_quantity` (this one will have null quantity)
"""
valid_quant = self.env['stock.quant'].create({
'product_id': self.product.id,
@@ -152,6 +153,7 @@ class TestEditableQuant(TransactionCase):
'location_id': self.room1.id,
'inventory_quantity': 20,
})
inventoried_quant.action_apply_inventory()
with self.assertRaises(UserError):
invalid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.product.id,
@@ -171,6 +173,7 @@ class TestEditableQuant(TransactionCase):
'quantity': 12,
})
quant.inventory_quantity = 24
quant.action_apply_inventory()
self.assertEqual(quant.quantity, 24)
stock_move = self.env['stock.move'].search([
('product_id', '=', self.product.id),
@@ -187,6 +190,7 @@ class TestEditableQuant(TransactionCase):
'quantity': 12,
})
quant.inventory_quantity = 8
quant.action_apply_inventory()
self.assertEqual(quant.quantity, 8)
stock_move = self.env['stock.move'].search([
('product_id', '=', self.product.id),
@@ -219,6 +223,7 @@ class TestEditableQuant(TransactionCase):
# Try to write on quant with permission
quant.with_user(user_admin).write({'inventory_quantity': 8})
quant.action_apply_inventory()
self.assertEqual(quant.quantity, 8)
def test_sn_warning(self):
@@ -237,7 +242,7 @@ class TestEditableQuant(TransactionCase):
'location_id': self.room1.id,
'inventory_quantity': 1,
'lot_id': sn1.id
})
}).action_apply_inventory()
dupe_sn = self.Quant.create({
'product_id': self.product_tracked_sn.id,
@@ -245,7 +250,7 @@ class TestEditableQuant(TransactionCase):
'inventory_quantity': 1,
'lot_id': sn1.id
})
dupe_sn.action_apply_inventory()
warning = False
warning = dupe_sn._onchange_serial_number()
self.assertTrue(warning, 'Reuse of existing serial number not detected')
+3 -3
View File
@@ -78,7 +78,7 @@ class TestReports(TestReportsCommon):
'product_id': product.id,
'location_id': stock.id,
'inventory_quantity': 50
})
}).action_apply_inventory()
self.env['stock.move'].flush()
report_records_today = self.env['report.stock.quantity'].read_group(
[('product_id', '=', product.id), ('date', '=', date.today())],
@@ -200,12 +200,12 @@ class TestReports(TestReportsCommon):
'product_id': product.id,
'location_id': stock.id,
'inventory_quantity': 50
})
}).action_apply_inventory()
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'location_id': stock_without_wh.id,
'inventory_quantity': 50
})
}).action_apply_inventory()
move = self.env['stock.move'].create({
'name': 'Move outside warehouse',
'location_id': stock.id,
+49 -149
View File
@@ -3,9 +3,9 @@
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.tests import Form
from odoo.tools import mute_logger, float_round
from odoo.exceptions import UserError
from odoo import fields
class TestStockFlow(TestStockCommon):
def setUp(cls):
super(TestStockFlow, cls).setUp()
@@ -117,7 +117,7 @@ class TestStockFlow(TestStockCommon):
# Check incoming shipment total quantity of pack operation
total_qty = sum(self.StockPackObj.search([('move_id', 'in', picking_in.move_lines.ids)]).mapped('qty_done'))
self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation')
self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation')
# Transfer Incoming Shipment.
picking_in._action_done()
@@ -1064,113 +1064,44 @@ class TestStockFlow(TestStockCommon):
total_qty = [quant.quantity for quant in quants]
self.assertAlmostEqual(sum(total_qty), 999.9975, msg='Expecting 999.9975 kg , got %.4f kg on location stock!' % (sum(total_qty)))
def test_20_create_inventory_with_different_uom(self):
"""Create inventory with different unit of measure."""
# ------------------------------------------------
# Test inventory with product A(Unit).
# ------------------------------------------------
inventory = self.InvObj.create({'name': 'Test',
'product_ids': [(4, self.UnitA.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
inventory_line = self.InvLineObj.create({
'inventory_id': inventory.id,
'product_id': self.UnitA.id,
'product_uom_id': self.uom_dozen.id,
'product_qty': 10,
'location_id': self.stock_location})
inventory.action_validate()
# Check quantity available of product UnitA.
quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
self.assertEqual(sum(total_qty), 120, 'Expecting 120 Units , got %.4f Units on location stock!' % (sum(total_qty)))
self.assertEqual(self.UnitA.qty_available, 120, 'Expecting 120 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available))
# Create Inventory again for product UnitA.
inventory = self.InvObj.create({'name': 'Test',
'product_ids': [(4, self.UnitA.id)]})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.")
inventory_line = self.InvLineObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1)
self.assertEqual(inventory_line.product_qty, 120, "Wrong product quantity in inventory line.")
# Modify the inventory line and set the quantity to 144 product on this new inventory.
inventory_line.write({'product_qty': 144})
inventory.action_validate()
move = self.MoveObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1)
self.assertEqual(move.product_uom_qty, 24, "Wrong move quantity of product UnitA.")
# Check quantity available of product UnitA.
quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
self.assertEqual(sum(total_qty), 144, 'Expecting 144 Units , got %.4f Units on location stock!' % (sum(total_qty)))
self.UnitA._compute_quantities()
self.assertEqual(self.UnitA.qty_available, 144, 'Expecting 144 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available))
# ------------------------------------------------
# Test inventory with product KG.
# ------------------------------------------------
productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id, 'type': 'product'})
inventory = self.InvObj.create({'name': 'Inventory Product KG',
'product_ids': [(4, productKG.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
inventory_line = self.InvLineObj.create({
'inventory_id': inventory.id,
'product_id': productKG.id,
'product_uom_id': self.uom_tone.id,
'product_qty': 5,
'location_id': self.stock_location})
inventory.action_validate()
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
self.assertEqual(sum(total_qty), 5000, 'Expecting 5000 kg , got %.4f kg on location stock!' % (sum(total_qty)))
self.assertEqual(productKG.qty_available, 5000, 'Expecting 5000 kg , got %.4f kg of quantity available!' % (productKG.qty_available))
# Create Inventory again.
inventory = self.InvObj.create({'name': 'Test',
'product_ids': [(4, productKG.id)]})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.")
inventory_line = self.InvLineObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
self.assertEqual(inventory_line.product_qty, 5000, "Wrong product quantity in inventory line.")
# Modify the inventory line and set the quantity to 4000 product on this new inventory.
inventory_line.write({'product_qty': 4000})
inventory.action_validate()
# Check inventory move quantity of product KG.
move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
self.assertEqual(move.product_uom_qty, 1000, "Wrong move quantity of product KG.")
# Check quantity available of product KG.
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty)))
productKG._compute_quantities()
self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available))
def test_20_create_inventory_with_packs_and_lots(self):
# --------------------------------------------------------
# TEST EMPTY INVENTORY WITH PACKS and LOTS
# ---------------------------------------------------------
packproduct = self.ProductObj.create({'name': 'Pack Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'})
lotproduct = self.ProductObj.create({'name': 'Lot Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'})
inventory = self.InvObj.create({'name': 'Test Partial and Pack',
'start_empty': True,
'location_ids': [(4, self.stock_location)]})
inventory.action_start()
quant_obj = self.env['stock.quant'].with_context(inventory_mode=True)
pack_obj = self.env['stock.quant.package']
lot_obj = self.env['stock.production.lot']
pack1 = pack_obj.create({'name': 'PACK00TEST1'})
pack_obj.create({'name': 'PACK00TEST2'})
lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id, 'company_id': self.env.company.id})
move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare")
line_vals = []
line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}]
line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id, 'package_id': pack1.id}]
line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 30, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}]
line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 25, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}]
inventory.write({'line_ids': [(0, 0, x) for x in line_vals]})
inventory.action_validate()
packproduct_no_pack_quant = quant_obj.create({
'product_id': packproduct.id,
'inventory_quantity': 10.0,
'location_id': self.stock_location
})
packproduct_quant = quant_obj.create({
'product_id': packproduct.id,
'inventory_quantity': 20.0,
'location_id': self.stock_location,
'package_id': pack1.id
})
lotproduct_no_lot_quant = quant_obj.create({
'product_id': lotproduct.id,
'inventory_quantity': 25.0,
'location_id': self.stock_location
})
lotproduct_quant = quant_obj.create({
'product_id': lotproduct.id,
'inventory_quantity': 30.0,
'location_id': self.stock_location,
'lot_id': lot1.id
})
(packproduct_no_pack_quant | packproduct_quant | lotproduct_no_lot_quant | lotproduct_quant).action_apply_inventory()
self.assertEqual(packproduct.qty_available, 30, "Wrong qty available for packproduct")
self.assertEqual(lotproduct.qty_available, 55, "Wrong qty available for lotproduct")
quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)])
@@ -1178,16 +1109,13 @@ class TestStockFlow(TestStockCommon):
self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty))
# Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack
inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2',
'start_empty': True,
'location_ids': [(4, self.stock_location)]})
inventory2.action_start()
line_vals = []
line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id}]
line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 0, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}]
line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 10, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}]
inventory2.write({'line_ids': [(0, 0, x) for x in line_vals]})
inventory2.action_validate()
packproduct_no_pack_quant.inventory_quantity = 20
lotproduct_no_lot_quant.inventory_quantity = 0
lotproduct_quant.inventory_quantity = 10
packproduct_no_pack_quant.action_apply_inventory()
lotproduct_no_lot_quant.action_apply_inventory()
lotproduct_quant.action_apply_inventory()
self.assertEqual(packproduct.qty_available, 40, "Wrong qty available for packproduct")
self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct")
quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)])
@@ -1513,17 +1441,12 @@ class TestStockFlow(TestStockCommon):
# "all at once" and "reserve" scenario
# -----------------------------------------------------------
# get one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
'product_qty': 1,
'location_id': self.stock_location
})]
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'inventory_quantity': 1,
})
inventory.action_start()
inventory.action_validate()
inventory_quant.action_apply_inventory()
# create a "all at once" delivery order for two products
picking_out = self.PickingObj.create({
@@ -1545,17 +1468,8 @@ class TestStockFlow(TestStockCommon):
self.assertEqual(picking_out.state, "confirmed")
# receive one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
'product_qty': 2,
'location_id': self.stock_location
})]
})
inventory.action_start()
inventory.action_validate()
inventory_quant.inventory_quantity = 2
inventory_quant.action_apply_inventory()
# recheck availability of the delivery order, it should be assigned
picking_out.action_assign()
self.assertEqual(len(picking_out.move_lines), 1.0)
@@ -1603,17 +1517,12 @@ class TestStockFlow(TestStockCommon):
# "partial" and "reserve" scenario
# -----------------------------------------------------------
# get one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
'product_qty': 1,
'location_id': self.stock_location
})]
inventory_quant = self.env['stock.quant'].create({
'location_id': self.stock_location,
'product_id': self.productA.id,
'inventory_quantity': 1,
})
inventory.action_start()
inventory.action_validate()
inventory_quant.action_apply_inventory()
# create a "partial" delivery order for two products
picking_out = self.PickingObj.create({
@@ -1636,17 +1545,8 @@ class TestStockFlow(TestStockCommon):
self.assertEqual(picking_out.state, "assigned")
# receive one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
'product_qty': 2,
'location_id': self.stock_location
})]
})
inventory.action_start()
inventory.action_validate()
inventory_quant.inventory_quantity = 2
inventory_quant.action_apply_inventory()
# recheck availability of the delivery order, it should be assigned
picking_out.action_assign()
+31 -34
View File
@@ -19,28 +19,29 @@ class TestWarehouse(TestStockCommon):
'inventory_quantity': 50.0,
'location_id': self.warehouse_1.lot_stock_id.id,
})
inventory = self.env['stock.inventory'].with_user(self.user_stock_manager).create({
'name': 'Starting for product_1',
'location_ids': [(4, self.warehouse_1.lot_stock_id.id)],
'product_ids': [(4, self.product_1.id)],
})
inventory.action_start()
# As done in common.py, there is already an inventory line existing
self.assertEqual(len(inventory.line_ids), 1)
self.assertEqual(inventory.line_ids.theoretical_qty, 50.0)
self.assertEqual(inventory.line_ids.product_id, self.product_1)
self.assertEqual(inventory.line_ids.product_uom_id, self.product_1.uom_id)
product_1_quant.action_apply_inventory()
# Update the line, set to 35
inventory.line_ids.write({'product_qty': 35.0})
inventory.action_validate()
# Make sure the inventory was successful
move_in_id = self.env['stock.move'].search([('is_inventory', '=', True), ('product_id', '=', self.product_1.id)])
self.assertEqual(len(move_in_id), 1)
self.assertEqual(move_in_id.product_qty, 50.0)
self.assertEqual(product_1_quant.quantity, 50.0)
self.assertEqual(move_in_id.product_uom, self.product_1.uom_id)
self.assertEqual(move_in_id.state, 'done')
# Update the inventory, set to 35
product_1_quant.inventory_quantity = 35.0
product_1_quant.action_apply_inventory()
# Check related move and quants
self.assertIn(inventory.name, inventory.move_ids.name)
self.assertEqual(inventory.move_ids.product_qty, 15.0)
self.assertEqual(inventory.move_ids.location_id, self.warehouse_1.lot_stock_id)
self.assertEqual(inventory.move_ids.location_dest_id, self.product_1.property_stock_inventory) # Inventory loss
self.assertEqual(inventory.move_ids.state, 'done')
move_ids = self.env['stock.move'].search([('is_inventory', '=', True), ('product_id', '=', self.product_1.id)])
self.assertEqual(len(move_ids), 2)
move_out_id = move_ids[-1]
self.assertEqual(move_out_id.product_qty, 15.0)
self.assertEqual(move_out_id.location_id, self.warehouse_1.lot_stock_id)
self.assertEqual(move_out_id.location_dest_id, self.product_1.property_stock_inventory) # Inventory loss
self.assertEqual(move_out_id.state, 'done')
quants = self.env['stock.quant']._gather(self.product_1, self.product_1.property_stock_inventory)
self.assertEqual(len(quants), 1) # One quant created for inventory loss
@@ -219,25 +220,21 @@ class TestWarehouse(TestStockCommon):
picking_out._action_done()
# Make an inventory adjustment to set the quantity to 0
inventory = self.env['stock.inventory'].create({
'name': 'Starting for product_1',
'location_ids': [(4, stock_location.id)],
'product_ids': [(4, productA.id)],
})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.")
inventory.line_ids.product_qty = 0 # Put the quantity back to 0
inventory.action_validate()
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(len(quant), 1, "Wrong number of quants created.")
self.assertEqual(quant.quantity, -1, "Theoretical quantity should be -1.")
# Put the quantity back to 0
quant.inventory_quantity = 0
quant.action_apply_inventory()
# The inventory adjustment should have created one
self.assertEqual(len(inventory.move_ids), 1)
quantity = inventory.move_ids.mapped('product_qty')
self.assertEqual(quantity, [1], "Moves created with wrong quantity.")
location_ids = inventory.move_ids.mapped('location_id').ids
self.assertEqual(set(location_ids), {location_loss.id})
move = self.env['stock.move'].search([('product_id', '=', productA.id), ('is_inventory', '=', True)])
self.assertEqual(len(move), 1)
self.assertEqual(move.product_qty, 1, "Moves created with wrong quantity.")
self.assertEqual(move.location_id.id, location_loss.id)
# There should be no quant in the stock location
self.env['stock.quant']._quant_tasks()
quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(sum(quants.mapped('quantity')), 0)
@@ -67,6 +67,18 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="annual_inventory_date" groups='stock.group_stock_manager'>
<div class="o_setting_right_pane">
<label for="annual_inventory_day" string="Annual Inventory Day and Month"/>
<div class="text-muted">
Day and month that annual inventory counts should occur.
</div>
<div class="content-group">
<field name="annual_inventory_day" class="o_light_label" style="width: 30px;"/>
<field name="annual_inventory_month" class="o_light_label"/>
</div>
</div>
</div>
</div>
<h2>Barcode</h2>
<div class="row mt16 o_settings_container" name="barcode_setting_container">
@@ -1,271 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_inventory_line_tree" model="ir.ui.view">
<field name="name">stock.inventory.line.tree</field>
<field name="model">stock.inventory.line</field>
<field name="arch" type="xml">
<tree default_order="location_id,product_id" editable="top" string="Inventory Details" js_class="inventory_validate_button"
decoration-danger="product_qty != theoretical_qty"
decoration-muted="product_qty == theoretical_qty"
decoration-bf="is_editable">
<field name="company_id" invisible="1"/>
<field name="product_tracking" invisible="1"/>
<field name="is_editable" invisible="1"/>
<field name="outdated" invisible="1"/>
<field name="product_id" width="1.6"
context="{'default_type': 'product'}"
readonly="context.get('default_product_id', False)"
attrs="{'readonly': ['|',
('is_editable', '=', False),
('state', '!=', 'confirm')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations"
options="{'no_create': True}"
invisible="context.get('readonly_location_id', False)"
readonly="context.get('readonly_location_id', False)"
attrs="{'readonly': ['|',
('is_editable', '=', False),
('state', '!=', 'confirm')]}"/>
<field name="inventory_date" optional="hide" width="0.8"/>
<field name="prod_lot_id" groups="stock.group_production_lot" width="0.8"
attrs="{'readonly': ['|', '|',
('product_tracking', '=', 'none'),
('is_editable', '=', False),
('state', '!=', 'confirm')]}"
context="{'default_product_id': product_id, 'default_company_id': company_id}" optional="show"/>
<field name="package_id" groups="stock.group_tracking_lot" width="0.8"
attrs="{'readonly': ['|',
('is_editable', '=', False),
('state', '!=', 'confirm')]}"
string="Package" optional="show"/>
<field name="partner_id" groups="stock.group_tracking_owner"
attrs="{'readonly': ['|',
('is_editable', '=', False),
('state', '!=', 'confirm')]}"/>
<field name="theoretical_qty" string="On Hand" width="0.5"
force_save="1" readonly="1" optional="show"/>
<button name="action_refresh_quantity" title="Refresh quantity"
icon="fa-refresh" type="object"
attrs="{'invisible': [('outdated', '=', False)]}"/>
<field name="product_qty" width="0.5" string="Counted"/>
<field name="difference_qty" optional="show" width="0.5"/>
<field name="product_uom_id" string="UoM" groups="uom.group_uom"
width="0.3" force_save="1"/>
<field name="inventory_id" invisible="1"/>
<field name="state" invisible="1"/>
</tree>
</field>
</record>
<record id="stock_inventory_line_tree_no_product_create" model="ir.ui.view">
<field name="name">stock.inventory.line.tree2</field>
<field name="model">stock.inventory.line</field>
<field name="inherit_id" ref="stock.stock_inventory_line_tree"/>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<field name="product_id" position="attributes"><attribute name="options">{'no_create': True}</attribute></field>
</field>
</record>
<record id="stock_inventory_line_search" model="ir.ui.view">
<field name="name">stock.inventory.line.search</field>
<field name="model">stock.inventory.line</field>
<field name="arch" type="xml">
<search string="Search Inventory Lines">
<field name="product_id"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="prod_lot_id" groups="stock.group_production_lot"/>
<field name="package_id" groups="stock.group_tracking_lot"/>
<field name="partner_id" groups="stock.group_tracking_owner"/>
<filter string="Outdated Theoretical Quantities"
name="outdated" domain="[('outdated', '=', True)]"/>
<filter string="Difference different than zero"
name="difference" domain="[('difference_qty', '!=', '0')]"/>
<group expand='0' string='Group by...'>
<filter string='Location' name="location" domain="[]" context="{'group_by' : 'location_id'}"/>
<filter string='Packaging' name="packaging" domain="[]" context="{'group_by' : 'package_id'}"/>
<filter string='Product' name="product" domain="[]" context="{'group_by' : 'product_id'}"/>
<filter string='Product Category' name="product_category" domain="[]" context="{'group_by' : 'categ_id'}"/>
</group>
</search>
</field>
</record>
<record id="view_inventory_filter" model="ir.ui.view">
<field name="name">stock.inventory.filter</field>
<field name="model">stock.inventory</field>
<field name="arch" type="xml">
<search string="Search Inventory">
<field name="name" string="Reference"/>
<field name="product_ids" string="Product" filter_domain="['|', ('product_ids', 'ilike', self), ('line_ids.product_id','ilike',self)]"/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="In Progress" name="confirm" domain="[('state', '=', 'confirm')]"/>
<filter string="Validated" name="done" domain="[('state', '=', 'done')]"/>
<separator/>
<filter string="Inventory Date" name="inventory_date" date="date"/>
<group expand="0" string="Group By">
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Inventory Date" name="inventories_month" domain="[]" context="{'group_by': 'date'}" help="Physical Inventories by Date"/>
</group>
</search>
</field>
</record>
<record id="view_inventory_tree" model="ir.ui.view">
<field name="name">stock.inventory.tree</field>
<field name="model">stock.inventory</field>
<field name="arch" type="xml">
<tree string="Lot/Serial Number Inventory" sample="1">
<field name="date"/>
<field name="name"/>
<field name="location_ids" widget="many2many_tags" optional="hide" groups="stock.group_stock_multi_locations"/>
<field name="product_ids" widget="many2many_tags" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state in ('draft', 'confirm')"/>
</tree>
</field>
</record>
<record id="view_stock_inventory_kanban" model="ir.ui.view">
<field name="name">stock.inventory.kanban</field>
<field name="model">stock.inventory</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" sample="1">
<field name="name"/>
<field name="date"/>
<field name="state"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong>
</div>
<field name="state" widget="label_selection" options="{'classes': {'draft': 'info', 'cancel': 'default', 'confirm': 'success', 'done': 'success'}}"/>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
</div>
<div class="oe_kanban_bottom_right">
<field name="date" widget="date"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_inventory_form" model="ir.ui.view">
<field name="name">stock.inventory.form</field>
<field name="model">stock.inventory</field>
<field name="arch" type="xml">
<form string="Inventory Adjustment">
<header>
<button name="action_start" states="draft" string="Start Inventory" type="object" class="oe_highlight" groups="stock.group_stock_user"/>
<button name="action_open_inventory_lines" states="confirm" string="Continue Inventory" type="object" class="oe_highlight" groups="stock.group_stock_user"/>
<button name="action_validate" string="Validate Inventory" type="object" attrs="{'invisible': ['|', ('state', '!=', 'confirm'), ('line_ids', '!=', [])]}" groups="stock.group_stock_manager"/>
<button name="action_validate" string="Validate Inventory" type="object" attrs="{'invisible': ['|', ('state', '!=', 'confirm'), ('line_ids', '=', [])]}" class="oe_highlight" groups="stock.group_stock_manager"/>
<button name="action_print" string="Print Count Sheet" type="object" attrs="{'invisible': [('state', '=', 'draft')]}" groups="stock.group_stock_user"/>
<button name="action_cancel_draft" states="cancel" string="Set to Draft" type="object"/>
<button name="action_cancel_draft" states="confirm" string="Cancel Inventory" type="object"
confirm="If you cancel this inventory adjustment, all its inventory adjustment lines will be lost. Are you sure you want to discard it ?"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,done"/>
</header>
<sheet>
<field name="line_ids" invisible="1"/>
<field name="is_conflict_inventory" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button string="Product Moves" type="object"
name="action_view_related_move_lines"
class="oe_stat_button" icon="fa-exchange"
attrs="{'invisible': [('state', '!=', 'done')]}"
context="{'no_reference': True}"/>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Annual inventory"/></h1>
</div>
<group>
<group>
<field name="location_ids" options="{'no_create': True}"
widget="many2many_tags"
force_save="1"
groups="stock.group_stock_multi_locations"/>
<field name="product_ids"
context="{'default_type': 'product'}"
widget="many2many_tags"
attrs="{'readonly': ['|', ('is_conflict_inventory', '=', True), ('state', '!=', 'draft')]}"/>
<field name="exhausted"
attrs="{'invisible': [('is_conflict_inventory', '=', True)]}"/>
</group>
<group>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"
attrs="{'readonly': ['|', ('is_conflict_inventory', '=', True), ('state', '!=', 'draft')]}"/>
<field name="prefill_counted_quantity" widget="radio"
attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_conflict_inventory', '=', True)]}"/>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="action_inventory_form" model="ir.actions.act_window">
<field name="name">Inventory Adjustments</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.inventory</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="view_inventory_tree"/>
<field name="search_view_id" ref="view_inventory_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_barcode_scanner">
Want to speed up your inventory counts? Try our Barcode app
</p><p>
Barcode scanner can be activated via inventory settings.
Manual inventory adjustments can also be performed and pre-filled with
suggested counted quantity.
</p>
</field>
</record>
<record id="action_open_inventory_adjustment_view" model="ir.actions.server">
<field name="name">Inventory Adjustments</field>
<field name="model_id" ref="model_stock_inventory"/>
<field name="state">code</field>
<field name="code">
action = model.action_open_inventory_view()
</field>
</record>
<record id="model_stock_inventory_line_action_reset_product_qty" model="ir.actions.server">
<field name="name">Set counted quantities to 0</field>
<field name="model_id" ref="model_stock_inventory_line"/>
<field name="binding_model_id" ref="model_stock_inventory_line"/>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_reset_product_qty()
</field>
</record>
<record id="model_stock_inventory_line_action_recompute_quantity" model="ir.actions.server">
<field name="name">Recompute On Hand Quantity</field>
<field name="model_id" ref="model_stock_inventory_line"/>
<field name="binding_model_id" ref="model_stock_inventory_line"/>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_refresh_quantity()
</field>
</record>
<menuitem action="action_open_inventory_adjustment_view" id="menu_action_inventory_form" parent="menu_stock_warehouse_mgmt" sequence="30"/>
</odoo>
+7 -1
View File
@@ -53,7 +53,7 @@
</div>
<field name="lot_id" groups="stock.group_production_lot" context="{'default_product_id': product_id, 'active_picking_id': picking_id, 'default_company_id': company_id}" attrs="{'invisible': [('lot_id', '=', False),('lot_name', '!=', False)]}"/>
<field name="lot_name" groups="stock.group_production_lot" attrs="{'invisible': ['|',('lot_id', '!=', False),('lot_name', '=', False)]}"/>
<field name="package_id" string="Source Package" groups="product.group_stock_packaging"/>
<field name="package_id" string="Source Package" groups="stock.group_tracking_lot"/>
<field name="result_package_id" string="Destination Package" groups="stock.group_tracking_lot"/>
<field name="owner_id" string="Owner" groups="stock.group_tracking_owner"/>
</group>
@@ -73,6 +73,10 @@
<field name="product_id"/>
<field name="location_id" string="Source Location" groups="stock.group_stock_multi_locations"/>
<field name="location_dest_id" string="Destination Location" groups="stock.group_stock_multi_locations"/>
<field name="lot_id" string="Lot/Serial Number" groups="stock.group_production_lot"/>
<field name="package_id" string="Source Package" groups="stock.group_tracking_lot"/>
<field name="result_package_id" string="Destination Package" groups="stock.group_tracking_lot"/>
<field name="owner_id" string="Owner" groups="stock.group_tracking_owner"/>
<separator/>
<filter string="To Do" name="todo" domain="[('state', 'not in', ['done', 'draft'])]"/>
<filter string="Done" name="done" domain="[('state', '=', 'done')]"/>
@@ -87,6 +91,8 @@
<filter string="Last 3 Months" name="filter_last_3_months" domain="[('date','&gt;=', (context_today() - relativedelta(months=3)).strftime('%%Y-%%m-%%d'))]"/>
<filter string="Last 12 Months" name="filter_last_12_months" domain="[('date','&gt;=', (context_today() - relativedelta(years=1)).strftime('%%Y-%%m-%%d'))]"/>
<separator/>
<filter string="Inventory" name="inventory" domain="[('is_inventory', '=', True)]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Product" name="groupby_product_id" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Status" name="by_state" domain="[]" context="{'group_by': 'state'}"/>
+4 -3
View File
@@ -90,7 +90,7 @@
<field name="product_uom_qty"/>
<field name="quantity_done"/>
<field name="reserved_availability"/>
<field name="inventory_id"/>
<field name="is_inventory"/>
<field name="state"/>
<field name="name"/>
<field name="product_uom"/>
@@ -108,11 +108,11 @@
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
</div>
<div class="o_kanban_record_body">
<div attrs="{'invisible': [('inventory_id', '=', False)]}">
<div attrs="{'invisible': [('is_inventory', '=', False)]}">
<span>Initial Demand <field name="product_uom_qty"/></span><br/>
<span>Quantity Done <field name="quantity_done"/></span>
</div>
<div attrs="{'invisible': [('inventory_id', '!=', False)]}">
<div attrs="{'invisible': [('is_inventory', '!=', False)]}">
<span>Quantity Done <field name="quantity_done"/>/<field name="reserved_availability"/></span>
</div>
</div>
@@ -346,6 +346,7 @@
<separator/>
<filter string="Incoming" name="incoming" domain="[('location_id.usage', 'not in', ('internal', 'transit')), ('location_dest_id.usage', 'in', ('internal', 'transit'))]"/>
<filter string="Outgoing" name="outgoing" domain="[('location_id.usage', 'in', ('internal', 'transit')), ('location_dest_id.usage', 'not in', ('internal', 'transit'))]"/>
<filter string="Inventory" name="inventory" domain="[('is_inventory', '=', True)]"/>
<separator/>
<filter string="Date" name="today" date="date" help="Scheduled or processing date"/>
<group expand="0" string="Group By">
+53 -5
View File
@@ -9,6 +9,8 @@
<search string="Quants">
<field name="product_id"/>
<field name="location_id"/>
<field name="user_id"/>
<field name="inventory_date"/>
<field name="package_id" groups="stock.group_tracking_lot"/>
<field name="lot_id" groups="stock.group_production_lot"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
@@ -22,6 +24,9 @@
<filter name="reserved" string="Reservations" domain="[('reserved_quantity', '&gt;', 0.0)]"/>
<separator/>
<filter name="filter_in_date" date="in_date"/>
<separator/>
<filter name="my_count" string="My Counts" domain="[('user_id', '=', uid)]"/>
<filter name="to_count" string="To Count" domain="[('inventory_date', '&lt;=', context_today().strftime('%Y-%m-%d'))]"/>
</group>
<group expand='0' string='Group by...'>
<filter string='Product' name="productgroup" context="{'group_by': 'product_id'}"/>
@@ -109,6 +114,7 @@
</field>
</record>
<!-- Inventory Report + Update Quantity (via product) view -->
<record model="ir.ui.view" id="view_stock_quant_tree_editable">
<field name="name">stock.quant.tree.editable</field>
<field name="model">stock.quant</field>
@@ -120,7 +126,6 @@
<field name="id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"
invisible="context.get('single_product', False)"
readonly="context.get('single_product', False)" force_save="1"
options="{'no_create': True}"/>
<field name="location_id" attrs="{'readonly': [('id', '!=', False)]}"
@@ -138,10 +143,9 @@
<field name="owner_id" groups="stock.group_tracking_owner"
attrs="{'readonly': [('id', '!=', False)]}"
options="{'no_create': True}"/>
<field name="quantity" string="On Hand Quantity"
invisible="1"/>
<field name="inventory_quantity" string="On Hand Quantity" decoration-danger="inventory_quantity &lt; 0"
<field name="inventory_quantity" string="Updated On Hand Quantity" decoration-danger="inventory_quantity &lt; 0"
readonly="0"/>
<field name="quantity" string="On Hand Quantity"/>
<field name="available_quantity" optional="show"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name='company_id' groups="base.group_multi_company" optional="show"/>
@@ -163,7 +167,7 @@
<field name="package_id" groups="stock.group_tracking_lot"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<field name="available_quantity"/>
<field name="quantity"/>
<field name="quantity" string="On Hand Quantity"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name='company_id' groups="base.group_multi_company"/>
</tree>
@@ -330,6 +334,50 @@
</field>
</record>
<!-- Inventory Adjustments view -->
<record model="ir.ui.view" id="view_stock_quant_tree_inventory_editable">
<field name="name">stock.quant.inventory.tree.editable</field>
<field name="model">stock.quant</field>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" editable="bottom" create="1" edit="1" import="0" js_class="singleton_list" sample="1">
<field name="id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="location_id" domain="[('usage', 'in', ['internal', 'transit'])]" attrs="{'readonly': [('id', '!=', False)]}" invisible="context.get('hide_location', False)" options="{'no_create': True}"/>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}" readonly="context.get('single_product', False)" force_save="1" options="{'no_create': True}"/>
<field name="lot_id" groups="stock.group_production_lot"
attrs="{'readonly': ['|', ('id', '!=', False), ('tracking', 'not in', ['serial', 'lot'])]}"
invisible="context.get('hide_lot', False)"
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
<field name="package_id" groups="stock.group_tracking_lot" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="owner_id" groups="stock.group_tracking_owner" attrs="{'readonly': [('id', '!=', False)]}" options="{'no_create': True}"/>
<field name="available_quantity" string="Available Quantity" decoration-danger="available_quantity &lt; 0" optional="hidden"/>
<field name="quantity" optional="show" string="On Hand Quantity"/>
<field name="product_uom_id" groups="uom.group_uom" string="UoM"/>
<field name="inventory_quantity" decoration-muted="inventory_quantity == 0"/>
<field name="inventory_diff_quantity" string="Difference" decoration-muted="inventory_quantity == 0" decoration-danger="inventory_diff_quantity &lt; 0" decoration-success="inventory_diff_quantity &gt; 0" decoration-bf="inventory_diff_quantity != 0"/>
<field name="inventory_date" optional="show"/>
<field name="user_id" string="User" optional="show"/>
<field name='company_id' groups="base.group_multi_company" optional="hide"/>
<button name="action_apply_inventory" type="object" string="Apply" class="btn btn-link" icon="fa-save" attrs="{'invisible': [('inventory_diff_quantity', '=', 0), ('inventory_quantity', '=', 0), ('quantity', '!=', 0)]}"/>
<button name="action_set_inventory_quantity" type="object" string="Set" class="btn btn-link" icon="fa-bullseye" attrs="{'invisible': ['|', '|', ('quantity', '=', 0.0), ('inventory_diff_quantity', '!=', 0), ('inventory_quantity', '!=', 0)]}"/>
<button name="action_inventory_history" type="object" class="btn btn-link" icon="fa-history" string="History"/>
<button name="action_set_inventory_quantity_to_zero" type="object" string="Reset" class="btn" icon="fa-times" attrs="{'invisible': [('inventory_diff_quantity', '=', 0), ('inventory_quantity', '=', 0), ('quantity', '!=', 0)]}"/>
</tree>
</field>
</record>
<record model="ir.actions.server" id="action_view_inventory_tree"> <!-- Menu + python -->
<field name="name">Inventory</field>
<field name="model_id" ref="model_stock_quant"/>
<field name="state">code</field>
<field name="groups_id" eval="[(4, ref('stock.group_stock_user'))]"/>
<field name="code">
action = model.action_view_inventory()
</field>
</record>
<menuitem id="menu_action_inventory_tree" name="Inventory Adjustments" parent="menu_stock_warehouse_mgmt" sequence="30" action="action_view_inventory_tree" />
<menuitem id="menu_package" name="Packages" parent="menu_stock_inventory_control" action="action_package_view" groups="stock.group_tracking_lot" sequence="102"/>
<menuitem id="menu_valuation" name="Inventory Report"
parent="stock.menu_warehouse_report" sequence="100"
+2
View File
@@ -4,6 +4,7 @@
from . import stock_assign_serial_numbers
from . import stock_picking_return
from . import stock_change_product_qty
from . import stock_inventory_conflict
from . import stock_scheduler_compute
from . import stock_immediate_transfer
from . import stock_backorder_confirmation
@@ -14,3 +15,4 @@ from . import product_replenish
from . import stock_track_confirmation
from . import stock_package_destination
from . import stock_orderpoint_snooze
from . import stock_request_count
@@ -39,5 +39,5 @@ class ProductChangeQuantity(models.TransientModel):
'product_id': self.product_id.id,
'location_id': warehouse.lot_stock_id.id,
'inventory_quantity': self.new_quantity,
})
})._apply_inventory()
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models
from odoo.exceptions import UserError
class StockInventoryConflict(models.TransientModel):
_name = 'stock.inventory.conflict'
_description = 'Conflict in Inventory'
quant_ids = fields.Many2many(
'stock.quant', 'stock_conflict_quant_rel', string='Quants')
quant_to_fix_ids = fields.Many2many(
'stock.quant', string='Conflicts')
def action_validate(self):
for conflict in self:
quant_to_fix_ids = conflict.quant_to_fix_ids.filtered(
lambda q: q.quantity != (q.inventory_quantity - q.inventory_diff_quantity))
if quant_to_fix_ids:
if self.user_has_groups('stock.group_stock_multi_locations'):
message = '\n'.join([_('%s at location %s', q.product_id.display_name, q.location_id.display_name) for q in quant_to_fix_ids])
else:
message = '\n'.join([_('%s', q.product_id.display_name) for q in quant_to_fix_ids])
raise UserError(
_('You still have conflicts to resolve:\n') + message)
conflict.quant_ids.action_apply_inventory()
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_inventory_conflict_form_view" model="ir.ui.view">
<field name="name">stock.inventory.conflict.form.view</field>
<field name="model">stock.inventory.conflict</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<div>
<strong>Some moves were made since your last inventory count. Plese count the inventory quantity again.</strong>
</div>
<div>
<br/>
<field name="quant_ids" invisible="1"/>
<field name="quant_to_fix_ids" style="margin-top:10px;">
<tree editable="bottom" create="0" delete="0">
<field name="id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}" invisible="context.get('single_product', False)" readonly="context.get('single_product', False)" force_save="1" options="{'no_create': True}"/>
<field name="location_id" attrs="{'readonly': [('id', '!=', False)]}" invisible="context.get('hide_location', False)" options="{'no_create': True}"/>
<field name="lot_id" groups="stock.group_production_lot" attrs="{
'readonly': ['|', ('id', '!=', False), ('tracking', 'not in', ['serial', 'lot'])],
'required': [('tracking', '!=', 'none')]
}" invisible="context.get('hide_lot', False)" context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
<field name="package_id" groups="stock.group_tracking_lot" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="owner_id" groups="stock.group_tracking_owner" attrs="{'readonly': [('id', '!=', False)]}" options="{'no_create': True}"/>
<field name="quantity" string="Quantity"/>
<field name="inventory_quantity" string="Counted Quantity" decoration-muted="inventory_quantity == 0" readonly="0"/>
<field name="inventory_diff_quantity" string="Difference" decoration-muted="inventory_quantity == 0" decoration-danger="inventory_diff_quantity &lt; 0" decoration-success="inventory_diff_quantity &gt; 0" decoration-bf="inventory_diff_quantity != 0"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name='company_id' groups="base.group_multi_company" optional="show"/>
</tree>
</field>
</div>
<footer>
<button name="action_validate" string="Confirm" type="object" class="btn-primary"/>
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockRequestCount(models.TransientModel):
_name = 'stock.request.count'
_description = 'Stock Request an Inventory Count'
inventory_date = fields.Date(
'Inventory Date', required=True,
help="Choose a date to get the inventory at that date",
default=fields.Datetime.now)
user_id = fields.Many2one('res.users', string="User")
quant_ids = fields.Many2many('stock.quant')
def action_request_count(self):
for count_request in self:
count_request.quant_ids.with_context(inventory_mode=True).write(
count_request._get_values_to_write())
def _get_values_to_write(self):
values = {
'inventory_date': self.inventory_date,
}
if self.user_id:
values['user_id'] = self.user_id.id,
return values
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_inventory_request_count_form_view" model="ir.ui.view">
<field name="name">stock.request.count.form.view</field>
<field name="model">stock.request.count</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<group>
<field name="quant_ids" invisible="1"/>
<field name="inventory_date"/>
<field name="user_id"/>
</group>
<footer>
<button name="action_request_count" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_stock_request_count" model="ir.actions.act_window">
<field name="name">Request a Count</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.request.count</field>
<field name="view_mode">form</field>
<field name="view_id" ref="stock_inventory_request_count_form_view"/>
<field name="target">new</field>
</record>
</odoo>
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class StockTrackConfirmation(models.TransientModel):
@@ -9,16 +9,28 @@ class StockTrackConfirmation(models.TransientModel):
_description = 'Stock Track Confirmation'
tracking_line_ids = fields.One2many('stock.track.line', 'wizard_id')
inventory_id = fields.Many2one('stock.inventory', 'Inventory')
quant_ids = fields.Many2many('stock.quant', string='Quants')
product_ids = fields.Many2many('product.product', string='Products')
def action_confirm(self):
for confirmation in self:
confirmation.inventory_id._action_done()
confirmation.quant_ids._apply_inventory()
@api.onchange('product_ids')
def _onchange_quants(self):
self.tracking_line_ids = [(0, 0, {'product_id': product}) for product in self.product_ids]
class StockTrackingLines(models.TransientModel):
_name = 'stock.track.line'
_description = 'Stock Track Line'
product_display_name = fields.Char('Name', compute='_compute_display_name', readonly=True)
product_id = fields.Many2one('product.product', 'Product', readonly=True)
tracking = fields.Selection([('lot', 'Tracked by lot'), ('serial', 'Tracked by serial number')], readonly=True)
tracking = fields.Selection(related='product_id.tracking')
wizard_id = fields.Many2one('stock.track.confirmation', readonly=True)
def _compute_display_name(self):
""" Onchange results in product.display_name not being directly accessible """
for line in self:
line.product_display_name = line.product_id._origin.display_name
@@ -4,13 +4,14 @@
<field name="name">stock.track.confirmation.view.form</field>
<field name="model">stock.track.confirmation</field>
<field name="arch" type="xml">
<form string="Lots or serial numbers were not provided to tracked products">
<field name="inventory_id" invisible="1"/>
<form string="Lots or serial numbers were not provided for tracked products">
<field name="product_ids" invisible="1"/>
<field name="quant_ids" invisible="1"/>
<p>Some products of the inventory adjustment are tracked. Are you sure you don't want to specify a serial or lot number for them?</p>
<strong>Product(s) tracked: </strong>
<strong>Tracked product(s):</strong>
<field name="tracking_line_ids" readonly="1">
<tree>
<field name="product_id"/>
<field name="product_display_name"/>
<field name="tracking"/>
</tree>
</field>
+1
View File
@@ -33,6 +33,7 @@ Dashboard / Reports for Warehouse Management includes:
'views/stock_quant_views.xml',
'views/report_invoice.xml',
'views/stock_valuation_layer_views.xml',
'wizard/stock_request_count.xml',
'wizard/stock_valuation_layer_revaluation_views.xml',
'report/report_stock_forecasted.xml',
],
-1
View File
@@ -5,7 +5,6 @@ from . import account_chart_template
from . import account_move
from . import product
from . import stock_move
from . import stock_inventory
from . import stock_location
from . import stock_move_line
from . import stock_picking
@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockInventory(models.Model):
_inherit = "stock.inventory"
accounting_date = fields.Date(
'Accounting Date',
help="Date at which the accounting entries will be created"
" in case of automated inventory valuation."
" If empty, the inventory date will be used.")
has_account_moves = fields.Boolean(compute='_compute_has_account_moves')
def _compute_has_account_moves(self):
for inventory in self:
if inventory.state == 'done' and inventory.move_ids:
account_move = self.env['account.move'].search_count([
('stock_move_id.id', 'in', inventory.move_ids.ids)
])
inventory.has_account_moves = account_move > 0
else:
inventory.has_account_moves = False
def action_get_account_moves(self):
self.ensure_one()
action_data = self.env['ir.actions.act_window']._for_xml_id('account.action_move_journal_line')
action_data['domain'] = [('stock_move_id.id', 'in', self.move_ids.ids)]
action_data['context'] = dict(self._context, create=False)
return action_data
def post_inventory(self):
res = True
acc_inventories = self.filtered(lambda inventory: inventory.accounting_date)
for inventory in acc_inventories:
res = super(StockInventory, inventory.with_context(force_period_date=inventory.accounting_date)).post_inventory()
other_inventories = self - acc_inventories
if other_inventories:
res = super(StockInventory, other_inventories).post_inventory()
return res
@@ -10,6 +10,11 @@ class StockQuant(models.Model):
value = fields.Monetary('Value', compute='_compute_value', groups='stock.group_stock_manager')
currency_id = fields.Many2one('res.currency', compute='_compute_value', groups='stock.group_stock_manager')
accounting_date = fields.Date(
'Accounting Date',
help="Date at which the accounting entries will be created"
" in case of automated inventory valuation."
" If empty, the inventory date will be used.")
@api.depends('company_id', 'location_id', 'owner_id', 'product_id', 'quantity')
def _compute_value(self):
@@ -55,3 +60,19 @@ class StockQuant(models.Model):
quants = self.search(group['__domain'])
group['value'] = sum(quant.value for quant in quants)
return res
def _apply_inventory(self):
acc_inventories = self.filtered(lambda quant: quant.accounting_date)
for inventory in acc_inventories:
super(StockQuant, self.with_context(force_period_date=inventory.accounting_date))._apply_inventory()
inventory.write({'accounting_date': False})
other_inventories = self - acc_inventories
if other_inventories:
super(StockQuant, other_inventories)._apply_inventory()
@api.model
def _get_inventory_fields_write(self):
""" Returns a list of fields user can edit when editing a quant in `inventory_mode`."""
res = super()._get_inventory_fields_write()
res += ['accounting_date']
return res
@@ -13,29 +13,12 @@
</field>
</record>
<record id="view_inventory_form_inherit" model="ir.ui.view">
<field name="name">stock.inventory.form.inherit</field>
<field name="model">stock.inventory</field>
<field name="inherit_id" ref="stock.view_inventory_form"/>
<record id="view_stock_quant_tree_inventory_editable_inherit_stock_account" model="ir.ui.view">
<field name="name">stock.quant.inventory.tree.editable.inherit.stock.account</field>
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.view_stock_quant_tree_inventory_editable"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="before">
<field name="accounting_date" attrs="{'readonly':[('state','=', 'done')]}"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<field name="has_account_moves" invisible="1"/>
<button name="action_get_account_moves" type="object"
string="Accounting Entries" icon="fa-usd" class="oe_stat_button"
attrs="{'invisible': [('has_account_moves', '=', False)]}"/>
</xpath>
</field>
</record>
<record id="view_inventory_tree" model="ir.ui.view">
<field name="name">stock.inventory.tree.inherit.stock.account</field>
<field name="model">stock.inventory</field>
<field name="inherit_id" ref="stock.view_inventory_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<xpath expr="//field[@name='product_uom_id']" position="after">
<field name="accounting_date" optional="hide"/>
</xpath>
</field>
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='product_uom_id']" position="after">
<field name="currency_id" invisible="1"/>
<field name="value"/>
<field name="value" optional="hidden"/>
</xpath>
</field>
</record>
@@ -19,7 +19,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='product_uom_id']" position="after">
<field name="currency_id" invisible="1"/>
<field name="value"/>
<field name="value" optional="hidden"/>
</xpath>
</field>
</record>
+1
View File
@@ -3,4 +3,5 @@
from . import stock_quantity_history
from . import stock_picking_return
from . import stock_request_count
from . import stock_valuation_layer_revaluation
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockRequestCount(models.TransientModel):
_inherit = 'stock.request.count'
accounting_date = fields.Date('Accounting Date')
def _get_values_to_write(self):
res = super()._get_values_to_write()
if self.accounting_date:
res['accounting_date'] = self.accounting_date
return res
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_inventory_request_count_form_view_inherit_stock_account" model="ir.ui.view">
<field name="name">stock.request.count.form.view.inherit.stock.account</field>
<field name="model">stock.request.count</field>
<field name="inherit_id" ref="stock.stock_inventory_request_count_form_view"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="accounting_date"/>
</field>
</field>
</record>
</odoo>
@@ -10,44 +10,29 @@
<field name="company_id" ref="base.main_company"/>
</record>
<!-- Resource: stock.inventory -->
<record id="stock_inventory_1" model="stock.inventory">
<field name="name">Starting Inventory</field>
</record>
<!-- Resource: stock.inventory.line -->
<record id="stock_inventory_line_0" model="stock.inventory.line">
<!-- Starting inventory -->
<record id="stock_inventory_batch_1" model="stock.quant">
<field name="product_id" ref="product.consu_delivery_01"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_picking_batch.stock_inventory_1"/>
<field name="product_qty">10.0</field>
<field name="inventory_quantity">10.0</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<record id="stock_inventory_line_1" model="stock.inventory.line">
<record id="stock_inventory_batch_2" model="stock.quant">
<field name="product_id" ref="product.consu_delivery_02"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_picking_batch.stock_inventory_1"/>
<field name="product_qty">10.0</field>
<field name="inventory_quantity">18.0</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<record id="stock_inventory_line_3" model="stock.inventory.line">
<record id="stock_inventory_batch_3" model="stock.quant">
<field name="product_id" ref="product.consu_delivery_03"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="inventory_id" ref="stock_picking_batch.stock_inventory_1"/>
<field name="product_qty">20.0</field>
<field name="inventory_quantity">20.0</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<!-- Inventory start -->
<function model="stock.inventory" name="_action_start">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_1'))]]" model="stock.inventory" name="search"/>
</function>
<!-- Inventory validate -->
<function model="stock.inventory" name="action_validate">
<function eval="[[('state','=','confirm'),('id', '=', ref('stock_inventory_1'))]]" model="stock.inventory" name="search"/>
</function>
<function model="stock.quant" name="action_apply_inventory">
<function eval="[[('id', 'in', (ref('stock_inventory_batch_1'),
ref('stock_inventory_batch_2'),
ref('stock_inventory_batch_3'),
))]]" model="stock.quant" name="search"/>
</function>
<!-- Add picking -->
<record id="Picking_A" model="stock.picking">
@@ -44,18 +44,19 @@ class TestWebsiteSaleStockProductWarehouse(TestWebsiteSaleProductAttributeValueC
})
# Update quantity of Product A in Warehouse 1
self.env['stock.quant'].with_context(inventory_mode=True).create({
quants = self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product_1.id,
'inventory_quantity': 10.0,
'location_id': warehouse_1.lot_stock_id.id,
})
# Update quantity of Product B in Warehouse 2
self.env['stock.quant'].with_context(inventory_mode=True).create({
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product_2.id,
'inventory_quantity': 10.0,
'location_id': warehouse_2.lot_stock_id.id,
})
quants.action_apply_inventory()
# Get current website and set warehouse_id of Warehouse 1
current_website = self.env['website'].get_current_website()