[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:
@@ -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 -->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
# ------------------------------------
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
# ---------------------------------------------------
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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','>=', (context_today() - relativedelta(months=3)).strftime('%%Y-%%m-%%d'))]"/>
|
||||
<filter string="Last 12 Months" name="filter_last_12_months" domain="[('date','>=', (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'}"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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', '>', 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', '<=', 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 < 0"
|
||||
<field name="inventory_quantity" string="Updated On Hand Quantity" decoration-danger="inventory_quantity < 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 < 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 < 0" decoration-success="inventory_diff_quantity > 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"
|
||||
|
||||
@@ -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 < 0" decoration-success="inventory_diff_quantity > 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>
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user