summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/mrp/data/mrp_demo.xml1
-rw-r--r--addons/mrp/tests/test_order.py4
-rw-r--r--addons/mrp/tests/test_stock.py8
-rw-r--r--addons/mrp/tests/test_traceability.py3
-rw-r--r--addons/mrp/tests/test_workorder_operation.py11
-rw-r--r--addons/mrp_account/tests/test_mrp_account.py1
-rw-r--r--addons/sale_mrp/tests/test_sale_mrp_flow.py6
-rw-r--r--addons/sale_stock/data/sale_order_demo.xml5
-rw-r--r--addons/stock/__manifest__.py1
-rw-r--r--addons/stock/data/stock_demo.xml7
-rw-r--r--addons/stock/data/stock_demo2.xml13
-rw-r--r--addons/stock/models/stock_inventory.py516
-rw-r--r--addons/stock/report/report_stockinventory.xml47
-rw-r--r--addons/stock/static/src/js/inventory_validate_button_controller.js95
-rw-r--r--addons/stock/static/src/js/inventory_validate_button_view.js16
-rw-r--r--addons/stock/static/src/xml/inventory_lines.xml8
-rw-r--r--addons/stock/tests/test_inventory.py379
-rw-r--r--addons/stock/tests/test_move2.py26
-rw-r--r--addons/stock/tests/test_stock_flow.py73
-rw-r--r--addons/stock/tests/test_warehouse.py10
-rw-r--r--addons/stock/views/stock_inventory_views.xml212
-rw-r--r--addons/stock/views/stock_move_line_views.xml8
-rw-r--r--addons/stock/views/stock_template.xml2
-rw-r--r--addons/stock_account/models/stock.py22
-rw-r--r--addons/stock_account/views/stock_account_views.xml10
25 files changed, 915 insertions, 569 deletions
diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml
index 37fc4e92777..6f6c7db8e07 100644
--- a/addons/mrp/data/mrp_demo.xml
+++ b/addons/mrp/data/mrp_demo.xml
@@ -509,7 +509,6 @@
<record id="stock_inventory_drawer" model="stock.inventory">
<field name="name">Inventory: Drawer + Kit</field>
- <field name="filter">partial</field>
</record>
<record id="stock_inventory_drawer_lot0" model="stock.inventory.line">
diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py
index 233d0bf4354..2736da29ee1 100644
--- a/addons/mrp/tests/test_order.py
+++ b/addons/mrp/tests/test_order.py
@@ -46,7 +46,6 @@ class TestMrpOrder(TestMrpCommon):
self.product_2.type = 'product'
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.product_1.id,
'product_uom_id': self.product_1.uom_id.id,
@@ -59,6 +58,7 @@ class TestMrpOrder(TestMrpCommon):
'location_id': self.warehouse_1.lot_stock_id.id
})]
})
+ inventory.action_start()
inventory.action_validate()
test_date_planned = Dt.now() - timedelta(days=1)
@@ -195,7 +195,6 @@ class TestMrpOrder(TestMrpCommon):
quant_before = custom_laptop.qty_available
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': product_charger.id,
'product_uom_id': product_charger.uom_id.id,
@@ -208,6 +207,7 @@ class TestMrpOrder(TestMrpCommon):
'location_id': source_location_id
})]
})
+ inventory.action_start()
inventory.action_validate()
# create a mo for this bom
diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py
index 1a11930ea24..b9678378f89 100644
--- a/addons/mrp/tests/test_stock.py
+++ b/addons/mrp/tests/test_stock.py
@@ -98,20 +98,18 @@ class TestWarehouse(common.TestMrpCommon):
stock_inv_product_4 = self.env['stock.inventory'].create({
'name': 'Stock Inventory for Stick',
- 'filter': 'product',
- 'product_id': self.product_4.id,
+ '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.ref('stock.stock_location_14')}),
]})
stock_inv_product_2 = self.env['stock.inventory'].create({
'name': 'Stock Inventory for Stone Tools',
- 'filter': 'product',
- 'product_id': self.product_2.id,
+ '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.ref('stock.stock_location_14')})
]})
- (stock_inv_product_4 | stock_inv_product_2).action_start()
+ (stock_inv_product_4 | stock_inv_product_2)._action_start()
stock_inv_product_2.action_validate()
stock_inv_product_4.action_validate()
diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py
index ed6c772f2e3..61aaa9c045b 100644
--- a/addons/mrp/tests/test_traceability.py
+++ b/addons/mrp/tests/test_traceability.py
@@ -26,8 +26,7 @@ class TestTraceability(TestMrpCommon):
stock_id = self.env.ref('stock.stock_location_stock').id
inventory_adjustment = self.env['stock.inventory'].create({
'name': 'Initial Inventory',
- 'location_id': stock_id,
- 'filter': 'partial',
+ 'location_ids': [(4, stock_id)],
})
inventory_adjustment.action_start()
inventory_adjustment.write({
diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py
index 530abcab062..b214fd3ad76 100644
--- a/addons/mrp/tests/test_workorder_operation.py
+++ b/addons/mrp/tests/test_workorder_operation.py
@@ -88,7 +88,6 @@ class TestWorkOrderProcess(TestMrpCommon):
# --------------------
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': product_table_sheet.id,
'product_uom_id': product_table_sheet.uom_id.id,
@@ -114,6 +113,7 @@ class TestWorkOrderProcess(TestMrpCommon):
'location_id': self.source_location_id
})]
})
+ inventory.action_start()
inventory.action_validate()
# Create work order
@@ -195,7 +195,6 @@ class TestWorkOrderProcess(TestMrpCommon):
# --------------------
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': product_table_sheet.id,
'product_uom_id': product_table_sheet.uom_id.id,
@@ -216,6 +215,7 @@ class TestWorkOrderProcess(TestMrpCommon):
'location_id': self.source_location_id
})]
})
+ inventory.action_start()
inventory.action_validate()
# Create work order
@@ -344,7 +344,6 @@ class TestWorkOrderProcess(TestMrpCommon):
# refuel stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory For Product C',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.product_2.id,
'product_uom_id': self.product_2.uom_id.id,
@@ -487,7 +486,6 @@ class TestWorkOrderProcess(TestMrpCommon):
# --------------------
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': product_charger.id,
'product_uom_id': product_charger.uom_id.id,
@@ -502,7 +500,7 @@ class TestWorkOrderProcess(TestMrpCommon):
'location_id': self.source_location_id
})]
})
- # inventory.action_start()
+ inventory.action_start()
inventory.action_validate()
# Check consumed move status
@@ -662,7 +660,6 @@ class TestWorkOrderProcess(TestMrpCommon):
# ----------------
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product B and C',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': product_B.id,
'product_uom_id': product_B.uom_id.id,
@@ -677,7 +674,7 @@ class TestWorkOrderProcess(TestMrpCommon):
'location_id': self.source_location_id
})]
})
- # inventory.action_start()
+ inventory.action_start()
inventory.action_validate()
# Start Production ...
diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py
index 64bba25019b..19ab942abb7 100644
--- a/addons/mrp_account/tests/test_mrp_account.py
+++ b/addons/mrp_account/tests/test_mrp_account.py
@@ -40,7 +40,6 @@ class TestMrpAccount(common.TransactionCase):
self.product_table_sheet.tracking = 'none'
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.product_table_sheet.id, # tracking serial
'product_uom_id': self.product_table_sheet.uom_id.id,
diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py
index 459892631b1..ddb931103dc 100644
--- a/addons/sale_mrp/tests/test_sale_mrp_flow.py
+++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py
@@ -407,8 +407,7 @@ class TestSaleMrpFlow(common.SavepointCase):
inventory = self.Inventory.create({
'name': 'Inventory Product KG',
- 'product_id': product_c.id,
- 'filter': 'product'})
+ 'product_ids': [(4, product_c.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
@@ -461,8 +460,7 @@ class TestSaleMrpFlow(common.SavepointCase):
# -------------------------------------------------------
inventory = self.Inventory.create({
'name': 'Inventory Product C KG',
- 'product_id': product_c.id,
- 'filter': 'product'})
+ 'product_ids': [(4, product_c.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
diff --git a/addons/sale_stock/data/sale_order_demo.xml b/addons/sale_stock/data/sale_order_demo.xml
index cf3d29808ca..6db1b10b172 100644
--- a/addons/sale_stock/data/sale_order_demo.xml
+++ b/addons/sale_stock/data/sale_order_demo.xml
@@ -45,8 +45,11 @@
<field name="location_id" ref="stock.stock_location_components"/>
</record>
- <function model="stock.inventory" name="action_validate">
+ <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>
</data>
</odoo>
diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index 38de8fed2b7..900e92ef6bb 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -76,6 +76,7 @@
'data/stock_sequence_data.xml',
],
'qweb': [
+ 'static/src/xml/inventory_lines.xml',
'static/src/xml/stock_traceability_report_backend.xml',
],
'installable': True,
diff --git a/addons/stock/data/stock_demo.xml b/addons/stock/data/stock_demo.xml
index 8a119887f22..82ba53ce5e0 100644
--- a/addons/stock/data/stock_demo.xml
+++ b/addons/stock/data/stock_demo.xml
@@ -119,12 +119,15 @@
<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="location_id" ref="stock_location_components"/>
+ <field name="location_id" ref="stock_location_components"/>
</record>
- <function model="stock.inventory" name="action_validate">
+ <function model="stock.inventory" name="action_start">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_0'))]]" model="stock.inventory" 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 -->
diff --git a/addons/stock/data/stock_demo2.xml b/addons/stock/data/stock_demo2.xml
index e8910052159..c629d788f26 100644
--- a/addons/stock/data/stock_demo2.xml
+++ b/addons/stock/data/stock_demo2.xml
@@ -45,7 +45,16 @@
<record id="stock_inventory_icecream" model="stock.inventory">
<field name="name">Inventory for Cable Management Box</field>
- <field name="filter">product</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"/>
+ </record>
+
+ <record id="lot_product_product_cable_management_box_1" model="stock.production.lot">
+ <field name="name">CM-BOX-00002</field>
<field name="product_id" ref="stock.product_cable_management_box"/>
</record>
@@ -55,6 +64,7 @@
<field name="inventory_id" ref="stock.stock_inventory_icecream"/>
<field name="product_qty">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"/>
</record>
<record id="stock_inventory_line_icecream_lot1" model="stock.inventory.line">
@@ -63,6 +73,7 @@
<field name="inventory_id" ref="stock.stock_inventory_icecream"/>
<field name="product_qty">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"/>
</record>
<!-- Create STOCK_MOVE for OUT -->
diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py
index 42d1b415085..305f9134c9c 100644
--- a/addons/stock/models/stock_inventory.py
+++ b/addons/stock/models/stock_inventory.py
@@ -4,7 +4,8 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
-from odoo.tools import float_utils, float_compare
+from odoo.osv import expression
+from odoo.tools import float_compare, float_is_zero
class Inventory(models.Model):
@@ -13,16 +14,21 @@ class Inventory(models.Model):
_order = "date desc, id desc"
@api.model
- def _default_location_id(self):
- company_user = self.env.company
- warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
- if warehouse:
- return warehouse.lot_stock_id.id
- else:
- raise UserError(_('You must define a warehouse for the company: %s.') % (company_user.name,))
+ def _default_location_ids(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'):
+ company_user = self.env.company
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
+ if warehouse:
+ return [(4, warehouse.lot_stock_id.id)]
+
+ @api.model
+ def _domain_location_ids(self):
+ return [('company_id', '=', self.env.company.id), ('usage', 'in', ['internal', 'transit'])]
name = fields.Char(
- 'Inventory Reference',
+ 'Inventory Reference', default="Inventory",
readonly=True, required=True,
states={'draft': [('readonly', False)]})
date = fields.Datetime(
@@ -50,137 +56,53 @@ class Inventory(models.Model):
readonly=True, index=True, required=True,
states={'draft': [('readonly', False)]},
default=lambda self: self.env.company)
- location_id = fields.Many2one(
- 'stock.location', 'Inventoried Location',
- readonly=True, required=True,
- states={'draft': [('readonly', False)]},
- default=_default_location_id)
- product_id = fields.Many2one(
- 'product.product', 'Inventoried Product',
- readonly=True,
- states={'draft': [('readonly', False)]},
- help="Specify Product to focus your inventory on a particular Product.")
- package_id = fields.Many2one(
- 'stock.quant.package', 'Inventoried Pack',
- readonly=True,
- states={'draft': [('readonly', False)]},
- help="Specify Pack to focus your inventory on a particular Pack.")
- partner_id = fields.Many2one(
- 'res.partner', 'Inventoried Owner',
+ location_ids = fields.Many2many(
+ 'stock.location', string='Inventoried Location(s)',
readonly=True,
states={'draft': [('readonly', False)]},
- help="Specify Owner to focus your inventory on a particular Owner.")
- lot_id = fields.Many2one(
- 'stock.production.lot', 'Inventoried Lot/Serial Number',
- copy=False, readonly=True,
+ default=lambda self: self._default_location_ids(),
+ domain=lambda self: self._domain_location_ids())
+ product_ids = fields.Many2many(
+ 'product.product', string='Inventoried Product(s)',
+ domain=[('type', '=', 'product')], readonly=True,
states={'draft': [('readonly', False)]},
- help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number.")
- filter = fields.Selection(
- string='Inventory of', selection='_selection_filter',
- required=True,
- default='none',
- help="If you do an entire inventory, you can choose 'All Products' and it will prefill the inventory with the current stock. If you only do some products "
- "(e.g. Cycle Counting) you can choose 'Manual Selection of Products' and the system won't propose anything. You can also let the "
- "system propose for a single product / lot /... ")
- total_qty = fields.Float('Total Quantity', compute='_compute_total_qty')
- category_id = fields.Many2one(
- 'product.category', 'Product Category',
- readonly=True, states={'draft': [('readonly', False)]},
- help="Specify Product Category to focus your inventory on a particular Category.")
- exhausted = fields.Boolean('Include Exhausted Products', readonly=True, states={'draft': [('readonly', False)]})
-
- @api.one
- @api.depends('product_id', 'line_ids.product_qty')
- def _compute_total_qty(self):
- """ For single product inventory, total quantity of the counted """
- if self.product_id:
- self.total_qty = sum(self.mapped('line_ids').mapped('product_qty'))
- else:
- self.total_qty = 0
+ help="Specify Products to focus your inventory on particular Products.")
+ start_empty = fields.Boolean('Empty Inventory',
+ help="Allows to start with an empty inventory.")
- @api.multi
def unlink(self):
for inventory in self:
if inventory.state == 'done':
raise UserError(_('You cannot delete a validated inventory adjustement.'))
return super(Inventory, self).unlink()
- @api.model
- def _selection_filter(self):
- """ Get the list of filter allowed according to the options checked
- in 'Settings\Warehouse'. """
- res_filter = [
- ('none', _('All products')),
- ('category', _('One product category')),
- ('product', _('One product only')),
- ('partial', _('Select products manually'))]
-
- if self.user_has_groups('stock.group_tracking_owner'):
- res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))]
- if self.user_has_groups('stock.group_production_lot'):
- res_filter.append(('lot', _('One Lot/Serial Number')))
- if self.user_has_groups('stock.group_tracking_lot'):
- res_filter.append(('pack', _('A Pack')))
- return res_filter
-
- @api.onchange('filter')
- def _onchange_filter(self):
- if self.filter not in ('product', 'product_owner'):
- self.product_id = False
- if self.filter != 'lot':
- self.lot_id = False
- if self.filter not in ('owner', 'product_owner'):
- self.partner_id = False
- if self.filter != 'pack':
- self.package_id = False
- if self.filter != 'category':
- self.category_id = False
- if self.filter == 'product':
- self.exhausted = True
- if self.product_id:
- return {'domain': {'product_id': [('product_tmpl_id', '=', self.product_id.product_tmpl_id.id)]}}
-
- @api.onchange('location_id')
- def _onchange_location_id(self):
- if self.location_id.company_id:
- self.company_id = self.location_id.company_id
-
- @api.one
- @api.constrains('filter', 'product_id', 'lot_id', 'partner_id', 'package_id')
- def _check_filter_product(self):
- if self.filter == 'none' and self.product_id and self.location_id and self.lot_id:
- return
- if self.filter not in ('product', 'product_owner') and self.product_id:
- raise ValidationError(_('The selected product doesn\'t belong to that owner..'))
- if self.filter != 'lot' and self.lot_id:
- raise ValidationError(_('The selected lot number doesn\'t exist.'))
- if self.filter not in ('owner', 'product_owner') and self.partner_id:
- raise ValidationError(_('The selected owner doesn\'t have the proprietary of that product.'))
- if self.filter != 'pack' and self.package_id:
- raise ValidationError(_('The selected inventory options are not coherent, the package doesn\'t exist.'))
-
- def action_reset_product_qty(self):
- self.mapped('line_ids').write({'product_qty': 0})
- return True
-
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',
- 'res_model': 'stock.track.confirmation',
- 'target': 'new',
- 'res_id': wiz.id,
- }
- else:
- self._action_done()
-
- def _action_done(self, cancel_backorder=False):
+ '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,
+ }
+ return self._action_done()
+
+ 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.name, negative.product_qty))
@@ -205,34 +127,81 @@ class Inventory(models.Model):
def action_cancel_draft(self):
self.mapped('move_ids')._action_cancel()
- self.write({
- 'line_ids': [(5,)],
- 'state': 'draft'
- })
+ self.write({'state': 'draft'})
def action_start(self):
- for inventory in self.filtered(lambda x: x.state not in ('done','cancel')):
- vals = {'state': 'confirm', 'date': fields.Datetime.now()}
- if (inventory.filter != 'partial') and not inventory.line_ids:
- vals.update({'line_ids': [(0, 0, line_values) for line_values in inventory._get_inventory_lines_values()]})
+ self.ensure_one()
+ self._action_start()
+ 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)
- return True
- def action_inventory_line_tree(self):
- action = self.env.ref('stock.action_inventory_line_tree').read()[0]
- action['context'] = {
- 'default_location_id': self.location_id.id,
- 'default_product_id': self.product_id.id,
- 'default_prod_lot_id': self.lot_id.id,
- 'default_package_id': self.package_id.id,
- 'default_partner_id': self.partner_id.id,
+ def action_open_inventory_lines(self):
+ self.ensure_one()
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'views': [(self.env.ref('stock.stock_inventory_line_tree2').id, 'tree')],
+ 'view_mode': 'tree',
+ 'name': _('Inventory Lines'),
+ 'res_model': 'stock.inventory.line',
+ }
+ context = {
+ 'default_is_editable': True,
'default_inventory_id': self.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:
+ if len(self.product_ids) == 1:
+ context['default_product_id'] = self.product_ids[0].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 _get_inventory_lines_values(self):
# TDE CLEANME: is sql really necessary ? I don't think so
- locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
+ locations = self.env['stock.location']
+ if self.location_ids:
+ locations = self.env['stock.location'].search([('id', 'child_of', self.location_ids.ids)])
+ else:
+ locations = self.env['stock.location'].search(self._domain_location_ids())
domain = ' location_id in %s AND quantity != 0 AND active = TRUE'
args = (tuple(locations.ids),)
@@ -240,37 +209,14 @@ class Inventory(models.Model):
Product = self.env['product.product']
# Empty recordset of products available in stock_quants
quant_products = self.env['product.product']
- # Empty recordset of products to filter
- products_to_filter = self.env['product.product']
- # case 0: Filter on company
+ # If inventory by company
if self.company_id:
domain += ' AND company_id = %s'
args += (self.company_id.id,)
-
- #case 1: Filter on One owner only or One product for a specific owner
- if self.partner_id:
- domain += ' AND owner_id = %s'
- args += (self.partner_id.id,)
- #case 2: Filter on One Lot/Serial Number
- if self.lot_id:
- domain += ' AND lot_id = %s'
- args += (self.lot_id.id,)
- #case 3: Filter on One product
- if self.product_id:
- domain += ' AND product_id = %s'
- args += (self.product_id.id,)
- products_to_filter |= self.product_id
- #case 4: Filter on A Pack
- if self.package_id:
- domain += ' AND package_id = %s'
- args += (self.package_id.id,)
- #case 5: Filter on One product category + Exahausted Products
- if self.category_id:
- categ_products = Product.search([('categ_id', 'child_of', self.category_id.id)])
- domain += ' AND product_id = ANY (%s)'
- args += (categ_products.ids,)
- products_to_filter |= categ_products
+ if self.product_ids:
+ domain += ' AND product_id in %s'
+ args += (tuple(self.product_ids.ids),)
self.env.cr.execute("""SELECT product_id, sum(quantity) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
FROM stock_quant
@@ -280,6 +226,7 @@ class Inventory(models.Model):
GROUP BY product_id, location_id, lot_id, package_id, partner_id """ % domain, args)
for product_data in self.env.cr.dictfetchall():
+ product_data['inventory_id'] = self.id
# replace the None the dictionary by False, because falsy values are tested later on
for void_field in [item[0] for item in product_data.items() if item[1] is None]:
product_data[void_field] = False
@@ -288,31 +235,6 @@ class Inventory(models.Model):
product_data['product_uom_id'] = Product.browse(product_data['product_id']).uom_id.id
quant_products |= Product.browse(product_data['product_id'])
vals.append(product_data)
- if self.exhausted:
- exhausted_vals = self._get_exhausted_inventory_line(products_to_filter, quant_products)
- vals.extend(exhausted_vals)
- return vals
-
- def _get_exhausted_inventory_line(self, products, quant_products):
- '''
- This function return inventory lines for exausted products
- :param products: products With Selected Filter.
- :param quant_products: products available in stock_quants
- '''
- vals = []
- exhausted_domain = [('type', 'not in', ('service', 'consu', 'digital'))]
- if products:
- exhausted_products = products - quant_products
- exhausted_domain += [('id', 'in', exhausted_products.ids)]
- else:
- exhausted_domain += [('id', 'not in', quant_products.ids)]
- exhausted_products = self.env['product.product'].search(exhausted_domain)
- for product in exhausted_products:
- vals.append({
- 'inventory_id': self.id,
- 'product_id': product.id,
- 'location_id': self.location_id.id,
- })
return vals
@@ -321,23 +243,48 @@ class InventoryLine(models.Model):
_description = "Inventory Line"
_order = "product_id, inventory_id, location_id, prod_lot_id"
+ @api.model
+ def _domain_location_id(self):
+ domain = self.env['stock.inventory']._domain_location_ids()
+ 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:
+ domain = expression.AND([
+ domain,
+ [('id', 'child_of', inventory.location_ids.ids)]
+ ])
+ return domain
+
+ @api.model
+ def _domain_product_id(self):
+ domain = [('type', '=', 'product')]
+ 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:
+ domain = expression.AND([
+ domain,
+ [('id', 'in', inventory.product_ids.ids)]
+ ])
+ return domain
+
+ is_editable = fields.Boolean(help="Technical field to restrict the edition.")
inventory_id = fields.Many2one(
'stock.inventory', 'Inventory',
index=True, ondelete='cascade')
partner_id = fields.Many2one('res.partner', 'Owner')
product_id = fields.Many2one(
'product.product', 'Product',
- domain=[('type', '=', 'product')],
+ domain=lambda self: self._domain_product_id(),
index=True, required=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Product Unit of Measure',
- required=True)
- product_uom_category_id = fields.Many2one(string='Uom category', related='product_uom_id.category_id', readonly=True)
+ required=True, readonly=True)
product_qty = fields.Float(
- 'Checked Quantity',
+ 'Counted Quantity',
digits=dp.get_precision('Product Unit of Measure'), default=0)
location_id = fields.Many2one(
'stock.location', 'Location',
+ domain=lambda self: self._domain_location_id(),
index=True, required=True)
package_id = fields.Many2one(
'stock.quant.package', 'Pack', index=True)
@@ -347,76 +294,124 @@ class InventoryLine(models.Model):
company_id = fields.Many2one(
'res.company', 'Company', related='inventory_id.company_id',
index=True, readonly=True, store=True)
- # TDE FIXME: necessary ? -> replace by location_id
- state = fields.Selection(
- 'Status', related='inventory_id.state', readonly=True)
+ state = fields.Selection('Status', related='inventory_id.state')
theoretical_qty = fields.Float(
- 'Theoretical Quantity', compute='_compute_theoretical_qty',
- digits=dp.get_precision('Product Unit of Measure'), readonly=True, store=True)
- inventory_location_id = fields.Many2one(
- 'stock.location', 'Inventory Location', related='inventory_id.location_id', related_sudo=False, readonly=False)
+ 'Theoretical Quantity',
+ digits=dp.get_precision('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=dp.get_precision('Product Unit of Measure'))
+ 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 oudated',
+ compute='_compute_outdated', search='_search_outdated')
product_tracking = fields.Selection('Tracking', related='product_id.tracking', readonly=True)
- @api.one
- @api.depends('location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id')
- def _compute_theoretical_qty(self):
- if not self.product_id:
- self.theoretical_qty = 0
- return
- 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,
- )
- self.theoretical_qty = theoretical_qty
-
- @api.onchange('product_id')
- def _onchange_product(self):
- res = {}
- # If no UoM or incorrect UoM put default one from product
- if self.product_id:
- self.product_uom_id = self.product_id.uom_id
- res['domain'] = {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
- return res
+ @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')
+ def _compute_outdated(self):
+ grouped_quants = self.env['stock.quant'].read_group(
+ [('product_id', 'in', self.product_id.ids), ('location_id', 'in', self.location_id.ids)],
+ ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'quantity:sum'],
+ ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id'],
+ lazy=False)
+ quants = {
+ (quant['product_id'][0],
+ quant['location_id'][0],
+ quant['lot_id'] and quant['lot_id'][0],
+ quant['package_id'] and quant['package_id'][0],
+ quant['owner_id'] and quant['owner_id'][0]): quant['quantity']
+ for quant in grouped_quants
+ }
+ for line in self:
+ if line.state == 'done' or not line.id:
+ 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
- self._compute_theoretical_qty()
- self.product_qty = self.theoretical_qty
+ 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
+ # We update `product_qty` only if it equals to `theoretical_qty` to
+ # avoid to reset quantity when user manually set it.
+ if self.product_qty == self.theoretical_qty:
+ self.product_qty = theoretical_qty
+ self.theoretical_qty = theoretical_qty
@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
- @api.multi
- def write(self,vals):
+ 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:
- existings = self.search([
+ domain = [
('id', '!=', line.id),
('product_id', '=', line.product_id.id),
- ('inventory_id.state', '=', 'confirm'),
('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)])
+ ('prod_lot_id', '=', line.prod_lot_id.id),
+ ('inventory_id', '=', line.inventory_id.id)]
+ existings = self.search_count(domain)
if existings:
- raise UserError(_("You cannot have two inventory adjustments in state 'In Progress' with the same product (%s),"
- " same location, same package, same owner and same lot. Please first validate"
- " the first inventory adjustment before creating another one.") % (line.product_id.display_name))
+ 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):
@@ -458,12 +453,47 @@ class InventoryLine(models.Model):
def _generate_moves(self):
vals_list = []
for line in self:
- if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
+ rounding = line.product_id.uom_id.rounding
+ if float_is_zero(line.difference_qty, precision_rounding=rounding):
continue
- diff = line.theoretical_qty - line.product_qty
- if diff < 0: # found more than expected
- vals = line._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id, False)
+ if line.difference_qty > 0: # found more than expected
+ vals = line._get_move_values(line.difference_qty, line.product_id.property_stock_inventory.id, line.location_id.id, False)
else:
- vals = line._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id, True)
+ vals = line._get_move_values(abs(line.difference_qty), line.location_id.id, line.product_id.property_stock_inventory.id, True)
vals_list.append(vals)
return self.env['stock.move'].create(vals_list)
+
+ def _refresh_inventory(self):
+ return self[0].inventory_id.action_open_inventory_lines()
+
+ 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_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)]
diff --git a/addons/stock/report/report_stockinventory.xml b/addons/stock/report/report_stockinventory.xml
index 19a988dfe59..2aff98f17dd 100644
--- a/addons/stock/report/report_stockinventory.xml
+++ b/addons/stock/report/report_stockinventory.xml
@@ -7,26 +7,14 @@
<t t-call="web.external_layout">
<div class="page">
<br/>
- <h2>Stock Inventory</h2>
+ <h2 t-field="o.name"/>
- <table class="table table-sm">
- <thead>
- <tr>
- <th><strong>Inventory</strong></th>
- <th><strong>Date</strong></th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>
- <span t-field="o.name"/>
- </td>
- <td>
- <span t-field="o.date"/>
- </td>
- </tr>
- </tbody>
- </table>
+ <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">
@@ -34,9 +22,10 @@
<tr>
<th groups="stock.group_stock_multi_locations"><strong>Location</strong></th>
<th><strong>Product</strong></th>
- <th groups="stock.group_production_lot"><strong>Production Lot</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>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>
@@ -52,26 +41,12 @@
<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>
-
- <t t-if="o.filter == 'product' and o.product_id">
- <div class="row justify-content-end">
- <div class="col-4">
- <table class="table table-sm">
- <tr class="border-black">
- <td><strong>Total Quantity</strong></td>
- <td class="text-right">
- <span t-field="o.total_qty"/> <span t-field="o.product_id.uom_id"/>
- </td>
- </tr>
- </table>
- </div>
- </div>
- </t>
</div>
</t>
</t>
diff --git a/addons/stock/static/src/js/inventory_validate_button_controller.js b/addons/stock/static/src/js/inventory_validate_button_controller.js
new file mode 100644
index 00000000000..ac5367b8d25
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_validate_button_controller.js
@@ -0,0 +1,95 @@
+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 ($node) {
+ this._super.apply(this, arguments);
+ var $validationButton = $(qweb.render('InventoryLines.Buttons'));
+ $validationButton.prependTo($node.find('.o_list_buttons'));
+ },
+
+ // -------------------------------------------------------------------------
+ // 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 === 'special') {
+ return;
+ }
+ // ... but in any other cases, we go back on the inventory form.
+ self.do_notify(
+ _t("Success"),
+ _t("The inventory has been validated"));
+ self.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'stock.inventory',
+ res_id: self.inventory_id,
+ views: [[false, 'form']],
+ target: 'main'
+ });
+ };
+
+ if (_.isObject(res)) {
+ self.do_action(res, { on_close: exitCallback });
+ } else {
+ return exitCallback();
+ }
+ });
+ });
+ },
+});
+
+return InventoryValidationController;
+
+});
diff --git a/addons/stock/static/src/js/inventory_validate_button_view.js b/addons/stock/static/src/js/inventory_validate_button_view.js
new file mode 100644
index 00000000000..ed5a5f42335
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_validate_button_view.js
@@ -0,0 +1,16 @@
+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);
+
+});
diff --git a/addons/stock/static/src/xml/inventory_lines.xml b/addons/stock/static/src/xml/inventory_lines.xml
new file mode 100644
index 00000000000..37fa0f5b79f
--- /dev/null
+++ b/addons/stock/static/src/xml/inventory_lines.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+ <t t-name="InventoryLines.Buttons">
+ <button type="button" class='btn btn-primary o_button_validate_inventory'>
+ Validate Inventory
+ </button>
+ </t>
+</templates>
diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py
index 730300d16d4..66f1048f019 100644
--- a/addons/stock/tests/test_inventory.py
+++ b/addons/stock/tests/test_inventory.py
@@ -37,9 +37,8 @@ class TestInventory(SavepointCase):
# remove them with an inventory adjustment
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product1.id,
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1)
@@ -56,22 +55,25 @@ class TestInventory(SavepointCase):
"""
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product2.id,
- 'exhausted': True, # should be set by an onchange
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
})
inventory.action_start()
- self.assertEqual(len(inventory.line_ids), 1)
- self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+ self.assertEqual(len(inventory.line_ids), 0)
lot1 = self.env['stock.production.lot'].create({
'name': 'sn2',
'product_id': self.product2.id,
})
-
- inventory.line_ids.prod_lot_id = lot1
- inventory.line_ids.product_qty = 1
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': self.stock_location.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 1
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
inventory.action_validate()
@@ -86,22 +88,25 @@ class TestInventory(SavepointCase):
"""
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product2.id,
- 'exhausted': True, # should be set by an onchange
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
})
inventory.action_start()
- self.assertEqual(len(inventory.line_ids), 1)
- self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+ self.assertEqual(len(inventory.line_ids), 0)
lot1 = self.env['stock.production.lot'].create({
'name': 'sn2',
'product_id': self.product2.id,
})
-
- inventory.line_ids.prod_lot_id = lot1
- inventory.line_ids.product_qty = 2
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': self.stock_location.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 2
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
with self.assertRaises(ValidationError):
inventory.action_validate()
@@ -112,22 +117,24 @@ class TestInventory(SavepointCase):
"""
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product2.id,
- 'exhausted': True, # should be set by an onchange
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
})
inventory.action_start()
- self.assertEqual(len(inventory.line_ids), 1)
- self.assertEqual(inventory.line_ids.theoretical_qty, 0)
-
+ self.assertEqual(len(inventory.line_ids), 0)
lot1 = self.env['stock.production.lot'].create({
'name': 'sn2',
'product_id': self.product2.id,
})
-
- inventory.line_ids.prod_lot_id = lot1
- inventory.line_ids.product_qty = 1
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 1,
+ 'location_id': self.stock_location.id,
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
@@ -136,6 +143,7 @@ class TestInventory(SavepointCase):
'product_qty': 10,
'location_id': self.stock_location.id,
})
+ self.assertEqual(len(inventory.line_ids), 2)
res_dict_for_warning_lot = inventory.action_validate()
wizard_warning_lot = self.env[(res_dict_for_warning_lot.get('res_model'))].browse(res_dict_for_warning_lot.get('res_id'))
wizard_warning_lot.action_confirm()
@@ -155,16 +163,21 @@ class TestInventory(SavepointCase):
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product1.id,
- 'exhausted': True,
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)]
})
inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 0)
+
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product1.id,
+ 'partner_id': owner1.id,
+ 'product_qty': 5,
+ 'location_id': self.stock_location.id,
+ })
self.assertEqual(len(inventory.line_ids), 1)
self.assertEqual(inventory.line_ids.theoretical_qty, 0)
- inventory.line_ids.partner_id = owner1
- inventory.line_ids.product_qty = 5
inventory.action_validate()
quant = self.env['stock.quant']._gather(self.product1, self.stock_location)
@@ -180,13 +193,16 @@ class TestInventory(SavepointCase):
# add 10 products in stock
inventory = self.env['stock.inventory'].create({
'name': 'add 10 products 1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product1.id,
- 'exhausted': True, # should be set by an onchange
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)]
})
inventory.action_start()
- inventory.line_ids.product_qty = 10
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product1.id,
+ 'product_qty': 10,
+ 'location_id': self.stock_location.id
+ })
inventory.action_validate()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
@@ -224,9 +240,8 @@ class TestInventory(SavepointCase):
# free the reservation of the second move.
inventory = self.env['stock.inventory'].create({
'name': 'remove 2 products 1',
- 'filter': 'product',
- 'location_id': self.pack_location.id,
- 'product_id': self.product1.id,
+ 'location_ids': [(4, self.pack_location.id)],
+ 'product_ids': [(4, self.product1.id)],
})
inventory.action_start()
inventory.line_ids.product_qty = 8
@@ -245,9 +260,8 @@ class TestInventory(SavepointCase):
# Make a new inventory adjustment and bring two now products.
inventory = self.env['stock.inventory'].create({
'name': 'remove 2 products 1',
- 'filter': 'product',
- 'location_id': self.pack_location.id,
- 'product_id': self.product1.id,
+ 'location_ids': [(4, self.pack_location.id)],
+ 'product_ids': [(4, self.product1.id)],
})
inventory.action_start()
inventory.line_ids.product_qty = 10
@@ -287,14 +301,275 @@ class TestInventory(SavepointCase):
self.env['stock.quant'].create(vals)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 2.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0)
-
+
inventory = self.env['stock.inventory'].create({
'name': 'product1',
- 'filter': 'product',
- 'location_id': self.stock_location.id,
- 'product_id': self.product1.id,
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
})
inventory.action_start()
self.assertEqual(len(inventory.line_ids), 1)
self.assertEqual(inventory.line_ids.theoretical_qty, 2)
+ def test_inventory_outdate_1(self):
+ """ Checks that inventory adjustment line is marked as outdated after
+ its corresponding quant is modify and its value was correctly updated
+ after user refreshed it.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ # When a inventory line is created, it must not be marked as outdated
+ # and its `theoretical_qty` must be equals to quant quantity.
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+
+ # Creates a new quant who'll update the existing one and so set product
+ # quantity to 5. Then expects inventory line has been marked as outdated.
+ vals = {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'inventory_quantity': 5,
+ }
+ self.env['stock.quant'].with_context(inventory_mode=True).create(vals)
+ self.assertEqual(inventory.line_ids.outdated, True)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 5
+ inventory.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 5)
+
+ def test_inventory_outdate_2(self):
+ """ Checks that inventory adjustment line is marked as outdated when a
+ quant is manually updated and its value is correctly updated when action
+ to refresh is called.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+
+ # Decreases quant to 3 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 3})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 3
+ inventory.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 3)
+
+ def test_inventory_outdate_3(self):
+ """ Checks that outdated inventory adjustment line without difference
+ doesn't change quant when validated.
+ """
+ # Set initial quantity to 10
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+
+ # increases quant to 15 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 15})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ # Don't refresh inventory line but valid it, and expect quantity is
+ # still equal to 15
+ inventory.action_validate()
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+ self.assertEqual(quant.quantity, 15)
+
+ def test_inventory_outdate_4(self):
+ """ Checks that outdated inventory adjustment line with difference
+ changes quant when validated.
+ """
+ # Set initial quantity to 10
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+
+ # increases quant to 15 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 15})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ # Don't refresh inventory line but changes its value and valid it, and
+ # expects quantity is correctly adapted (15 + inventory line diff)
+ inventory.line_ids.product_qty = 12
+ inventory.action_validate()
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+ self.assertEqual(quant.quantity, 17)
+
+ def test_inventory_outdate_5(self):
+ """ Checks that inventory adjustment line is marked as outdated when an
+ another inventory adjustment line with common product/location is
+ validated and its value is updated when action to refresh is called.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+
+ inventory_1 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_1.action_start()
+ inventory_2 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_2.action_start()
+ self.assertEqual(inventory_1.line_ids.outdated, False)
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, inventory_2.line_ids.theoretical_qty)
+
+ # Set product quantity to 8 in inventory 2 then validates it
+ inventory_2.line_ids.product_qty = 8
+ inventory_2.action_validate()
+ # Expects line of inventory 1 is now marked as outdated
+ self.assertEqual(inventory_1.line_ids.outdated, True)
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 8
+ inventory_1.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, 8)
+
+ def test_inventory_dont_outdate_1(self):
+ """ Checks that inventory adjustment line isn't marked as outdated when
+ a not corresponding quant is created.
+ """
+ # Set initial quantity to 7 and create inventory adjustment for product1
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+
+ # Create quant for product3
+ product3 = self.env['product.product'].create({
+ 'name': 'Product C',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ vals = {
+ 'product_id': product3.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'inventory_quantity': 22,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+ # Expect inventory line is still up to date
+ self.assertEqual(inventory.line_ids.outdated, False)
+
+ def test_inventory_dont_outdate_2(self):
+ """ Checks that inventory adjustment line isn't marked as outdated when
+ an another inventory adjustment line without common product/location is
+ validated.
+ """
+ # Set initial quantity for product1 and product3
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ product3 = self.env['product.product'].create({
+ 'name': 'Product C',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': product3.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ })
+
+ inventory_1 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_1.action_start()
+ inventory_2 = self.env['stock.inventory'].create({
+ 'name': 'product3',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, product3.id)],
+ })
+ inventory_2.action_start()
+ self.assertEqual(inventory_1.line_ids.outdated, False)
+
+ # Set product3 quantity to 16 in inventory 2 then validates it
+ inventory_2.line_ids.product_qty = 16
+ inventory_2.action_validate()
+ # Expect line of inventory 1 is still up to date
+ self.assertEqual(inventory_1.line_ids.outdated, False)
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index cb97d657e5c..53246be2705 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -1130,9 +1130,8 @@ class TestSinglePicking(TestStockCommon):
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location,
- 'product_id': self.productA.id,
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
})
inventory.action_start()
inventory.line_ids.product_qty = 2
@@ -1187,9 +1186,8 @@ class TestSinglePicking(TestStockCommon):
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location,
- 'product_id': self.productA.id,
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
})
inventory.action_start()
inventory.line_ids.prod_lot_id = lot1
@@ -1247,15 +1245,13 @@ class TestSinglePicking(TestStockCommon):
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location,
- 'product_id': self.productA.id,
+ '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_id.id,
- 'partner_id': inventory.partner_id.id,
+ 'location_id': inventory.location_ids[0].id,
'prod_lot_id': lot2.id,
'product_id': self.productA.id,
'product_qty': 1,
@@ -1314,15 +1310,13 @@ class TestSinglePicking(TestStockCommon):
inventory = self.env['stock.inventory'].create({
'name': 'remove product1',
- 'filter': 'product',
- 'location_id': self.stock_location,
- 'product_id': self.productA.id,
+ '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_id.id,
- 'partner_id': inventory.partner_id.id,
+ 'location_id': inventory.location_ids[0].id,
'prod_lot_id': serial2.id,
'product_id': self.productA.id,
'product_qty': 1,
diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py
index 1b0aef059b5..0fe521d992c 100644
--- a/addons/stock/tests/test_stock_flow.py
+++ b/addons/stock/tests/test_stock_flow.py
@@ -1067,8 +1067,7 @@ class TestStockFlow(TestStockCommon):
# ------------------------------------------------
inventory = self.InvObj.create({'name': 'Test',
- 'product_id': self.UnitA.id,
- 'filter': 'product'})
+ 'product_ids': [(4, self.UnitA.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
inventory_line = self.InvLineObj.create({
@@ -1085,8 +1084,7 @@ class TestStockFlow(TestStockCommon):
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_id': self.UnitA.id,
- 'filter': 'product'})
+ '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)
@@ -1109,8 +1107,7 @@ class TestStockFlow(TestStockCommon):
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_id': productKG.id,
- 'filter': 'product'})
+ 'product_ids': [(4, productKG.id)]})
inventory.action_start()
self.assertFalse(inventory.line_ids, "Inventory line should not created.")
inventory_line = self.InvLineObj.create({
@@ -1126,8 +1123,7 @@ class TestStockFlow(TestStockCommon):
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_id': productKG.id,
- 'filter': 'product'})
+ '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)
@@ -1146,14 +1142,14 @@ class TestStockFlow(TestStockCommon):
self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available))
# --------------------------------------------------------
- # TEST PARTIAL INVENTORY WITH PACKS and LOTS
+ # 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',
- 'filter': 'partial',
- 'location_id': self.stock_location})
+ 'start_empty': True,
+ 'location_ids': [(4, self.stock_location)]})
inventory.action_start()
pack_obj = self.env['stock.quant.package']
lot_obj = self.env['stock.production.lot']
@@ -1178,8 +1174,8 @@ class TestStockFlow(TestStockCommon):
# 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',
- 'filter': 'partial',
- 'location_id': self.stock_location})
+ '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}]
@@ -1196,49 +1192,6 @@ class TestStockFlow(TestStockCommon):
total_qty = sum([quant.quantity for quant in quants])
self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty))
- # check product available of saleable category in stock location
- category_id = self.ref('product.product_category_5')
- inventory3 = self.InvObj.create({
- 'name': 'Test Category',
- 'filter': 'category',
- 'location_id': self.stock_location,
- 'category_id': category_id
- })
- # Start Inventory
- inventory3.action_start()
- # check all products have given category id
- products_category = inventory3.line_ids.mapped('product_id.categ_id')
- self.assertEqual(len(products_category), 1, "Inventory line should have only one category")
- inventory3.action_validate()
- # check category with exhausted in stock location
- inventory4 = self.InvObj.create({
- 'name': 'Test Exhausted Product',
- 'filter': 'category',
- 'location_id': self.stock_location,
- 'category_id': category_id,
- 'exhausted': True,
- })
- inventory4.action_start()
- inventory4._get_inventory_lines_values()
- inventory4_lines_count = len(inventory4.line_ids)
- inventory4.action_validate()
- # Add one product in this product category
- product = self.ProductObj.create({'name': 'Product A', 'type': 'product', 'categ_id': category_id})
- # Check that this exhausted product is in the product category inventory adjustment
- inventory5 = self.InvObj.create({
- 'name': 'Test Exhausted Product',
- 'filter': 'category',
- 'location_id': self.stock_location,
- 'category_id': category_id,
- 'exhausted': True,
- })
- inventory5.action_start()
- inventory5._get_inventory_lines_values()
- inventory5_lines_count = len(inventory5.line_ids)
- inventory5.action_validate()
- self.assertEqual(inventory5_lines_count, inventory4_lines_count + 1, "The new product is not taken into account in the inventory valuation.")
- self.assertTrue(product.id in inventory5.line_ids.mapped('product_id').ids, "The new product is not take into account in the inventory valuation.")
-
def test_30_check_with_no_incoming_lot(self):
""" Picking in without lots and picking out with"""
# Change basic operation type not to get lots
@@ -1565,7 +1518,6 @@ class TestStockFlow(TestStockCommon):
# get one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
@@ -1573,6 +1525,7 @@ class TestStockFlow(TestStockCommon):
'location_id': self.stock_location
})]
})
+ inventory.action_start()
inventory.action_validate()
# create a "all at once" delivery order for two products
@@ -1598,7 +1551,6 @@ class TestStockFlow(TestStockCommon):
# receive one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
@@ -1606,6 +1558,7 @@ class TestStockFlow(TestStockCommon):
'location_id': self.stock_location
})]
})
+ inventory.action_start()
inventory.action_validate()
# recheck availability of the delivery order, it should be assigned
picking_out.action_assign()
@@ -1657,7 +1610,6 @@ class TestStockFlow(TestStockCommon):
# get one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
@@ -1665,6 +1617,7 @@ class TestStockFlow(TestStockCommon):
'location_id': self.stock_location
})]
})
+ inventory.action_start()
inventory.action_validate()
# create a "partial" delivery order for two products
@@ -1691,7 +1644,6 @@ class TestStockFlow(TestStockCommon):
# receive one product in stock
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product Table',
- 'filter': 'partial',
'line_ids': [(0, 0, {
'product_id': self.productA.id,
'product_uom_id': self.productA.uom_id.id,
@@ -1699,6 +1651,7 @@ class TestStockFlow(TestStockCommon):
'location_id': self.stock_location
})]
})
+ inventory.action_start()
inventory.action_validate()
# recheck availability of the delivery order, it should be assigned
diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py
index 1cd942ae701..a0b99724aed 100644
--- a/addons/stock/tests/test_warehouse.py
+++ b/addons/stock/tests/test_warehouse.py
@@ -14,9 +14,8 @@ class TestWarehouse(TestStockCommon):
})
inventory = self.env['stock.inventory'].sudo(self.user_stock_manager).create({
'name': 'Starting for product_1',
- 'filter': 'product',
- 'location_id': self.warehouse_1.lot_stock_id.id,
- 'product_id': self.product_1.id,
+ '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
@@ -175,9 +174,8 @@ class TestWarehouse(TestStockCommon):
# Make an inventory adjustment to set the quantity to 0
inventory = self.env['stock.inventory'].create({
'name': 'Starting for product_1',
- 'filter': 'product',
- 'location_id': stock_location.id,
- 'product_id': productA.id,
+ '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.")
diff --git a/addons/stock/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml
index 3170a4bb7c6..a80f2e3568c 100644
--- a/addons/stock/views/stock_inventory_views.xml
+++ b/addons/stock/views/stock_inventory_views.xml
@@ -18,19 +18,48 @@
<field name="name">stock.inventory.line.tree2</field>
<field name="model">stock.inventory.line</field>
<field name="arch" type="xml">
- <tree editable="top" string="Inventory Details" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty &lt; 0">
+ <tree editable="top" string="Inventory Details" js_class="inventory_validate_button"
+ decoration-info="product_qty != theoretical_qty" decoration-danger="outdated"
+ decoration-bf="is_editable">
<field name="product_tracking" invisible="1"/>
- <field name="product_id" domain="[('type','=','product')]"/>
- <field name="product_uom_id" string="UoM" groups="uom.group_uom"/>
- <field name="location_id" options="{'no_create': True}" domain="[('id', 'child_of', inventory_location_id)]" groups="stock.group_stock_multi_locations"/>
- <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
- <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/>
- <field name="partner_id" groups="stock.group_tracking_owner"/>
- <field name="theoretical_qty" readonly="1"/>
- <field name="product_qty" string="Real Quantity"/>
- <field name="state" invisible="1"/>
+ <field name="is_editable" invisible="1"/>
+ <field name="outdated" invisible="1"/>
+ <field name="product_id" options="{'no_create': True}"
+ 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}"
+ readonly="context.get('readonly_location_id', False)"
+ attrs="{'readonly': ['|',
+ ('is_editable', '=', False),
+ ('state', '!=', 'confirm')]}"/>
+ <field name="prod_lot_id" groups="stock.group_production_lot"
+ attrs="{'readonly': ['|', '|',
+ ('product_tracking', '=', 'none'),
+ ('is_editable', '=', False),
+ ('state', '!=', 'confirm')]}"
+ context="{'default_product_id': product_id}"
+ domain="[('product_id', '=', product_id)]"/>
+ <field name="package_id" groups="stock.group_tracking_lot"
+ attrs="{'readonly': ['|',
+ ('is_editable', '=', False),
+ ('state', '!=', 'confirm')]}"
+ string="Package"
+ domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]"/>
+ <field name="partner_id" groups="stock.group_tracking_owner"
+ attrs="{'readonly': ['|',
+ ('is_editable', '=', False),
+ ('state', '!=', 'confirm')]}"/>
+ <field name="theoretical_qty" string="On Hand Quantity"
+ force_save="1" readonly="1"/>
+ <field name="product_qty" attrs="{'readonly': [('state', '!=', 'confirm')]}"/>
+ <field name="difference_qty"/>
+ <field name="product_uom_id" string="UoM" groups="uom.group_uom" force_save="1"/>
+ <field name="inventory_date"/>
<field name="inventory_id" invisible="1"/>
- <field name="inventory_location_id" options="{'no_create': True}" invisible="1"/>
+ <field name="state" invisible="1"/>
</tree>
</field>
</record>
@@ -45,6 +74,12 @@
<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)]"/>
+ <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'}"/>
+ </group>
</search>
</field>
</record>
@@ -57,7 +92,7 @@
<field name="name" string="Inventory Reference"/>
<field name="date"/>
<field name="company_id" groups="base.group_multi_company"/>
- <field name="product_id" string="Inventory Product" filter_domain="['|', ('product_id', 'ilike', self), ('line_ids.product_id','ilike',self)]"/>
+ <field name="product_ids" string="Inventory 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')]"/>
@@ -118,6 +153,7 @@
<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" states="confirm" string="Validate Inventory" type="object" class="oe_highlight" groups="stock.group_stock_manager"/>
<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"/>
@@ -125,17 +161,11 @@
</header>
<sheet>
<div class="oe_button_box" name="button_box">
- <button name="action_inventory_line_tree"
- class="oe_stat_button"
- icon="fa-building-o"
- type="object"
- help="List view of lines"
- groups="base.group_no_one"
- states="confirm">
- <div class="o_form_field o_stat_info">
- <span class="o_stat_text">Details</span>
- </div>
- </button>
+ <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" class="oe_edit_only"/>
@@ -143,105 +173,25 @@
</div>
<group>
<group>
- <field name="location_id" options="{'no_create': True}" domain="[('usage','not in', ['supplier','production'])]" groups="stock.group_stock_multi_locations"/>
- <field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}"/>
+ <field name="location_ids" options="{'no_create': True}"
+ widget="many2many_tags"
+ groups="stock.group_stock_multi_locations"/>
+ <field name="product_ids" options="{'no_create': True}"
+ widget="many2many_tags"/>
</group>
<group>
- <field name="date"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
- <field name="product_id" domain="[('type','=','product')]" attrs="{'invisible': [('filter', 'not in', ('product', 'product_owner'))], 'required': [('filter', 'in', ('product', 'product_owner'))]}" options="{'no_create': True}"/>
- <field name="category_id" attrs="{'invisible': [('filter', 'not in','category')], 'required': [('filter', 'in', 'category')]}" options="{'no_create': True}"/>
- <field name="lot_id" attrs="{'invisible': [('filter', '!=', 'lot')], 'required': [('filter', '=', 'lot')]}" groups="stock.group_production_lot" />
- <field name="partner_id" attrs="{'invisible': [('filter', 'not in', ('owner', 'product_owner'))], 'required': [('filter', 'in', ('owner', 'product_owner'))]}" groups="stock.group_tracking_owner"/>
- <field name="package_id" attrs="{'invisible': [('filter', '!=', 'pack')], 'required': [('filter', '=', 'pack')]}" groups="stock.group_tracking_lot"/>
- <field name="exhausted" attrs="{'invisible': [('filter', 'in', ('owner', 'product_owner','lot','pack','partial', 'product'))]}"/>
</group>
</group>
- <notebook attrs="{'invisible':[('state','=','draft')]}">
- <page string="Inventory Details" >
- <button name="action_reset_product_qty" states="confirm" string="⇒ Set quantities to 0" type="object" class="oe_link oe_right" groups="stock.group_stock_user"/>
- <field name="line_ids" string="Inventory Details" context="{'default_location_id': location_id, 'default_product_id': product_id, 'default_prod_lot_id': lot_id, 'default_package_id': package_id, 'default_partner_id': partner_id}" mode="tree,kanban">
- <tree string="Inventory Details" editable="bottom" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty &lt; 0">
- <field name="product_tracking" invisible="1"/>
- <field name="product_id" domain="[('type','=','product')]" attrs="{'readonly': [('parent.filter', '=', 'product')]}" force_save="1"/>
- <field name="product_uom_id" string="UoM" groups="uom.group_uom" domain="[('category_id', '=', product_uom_category_id)]"/>
- <field name="product_uom_category_id" invisible="1"/>
- <field name="location_id" options="{'no_create': True}" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations"/>
- <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
- <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/>
- <field name="partner_id" groups="stock.group_tracking_owner"/>
- <field name="theoretical_qty" readonly="1"/>
- <field name="product_qty" string="Real Quantity"/>
- <field name="state" invisible="True"/>
- </tree>
- <kanban class="o_kanban_mobile">
- <field name="product_tracking" invisible="1"/>
- <field name="product_id" domain="[('type','=','product')]"/>
- <field name="product_uom_id" groups="uom.group_uom" string="UoM"/>
- <field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations"/>
- <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
- <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]"/>
- <field name="partner_id"/>
- <field name="theoretical_qty" readonly="1"/>
- <field name="product_qty" string="Real Quantity"/>
- <field name="state" invisible="True"/>
-
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click_edit
- #{record.product_qty.raw_value!=record.theoretical_qty.raw_value ? 'oe_kanban_color_6' : ''}
- #{record.theoretical_qty.raw_value&lt;0 ? 'oe_kanban_color_2' : ''}">
- <div class="row">
- <div class="col-12">
- <strong>
- <field name='product_id'/>
- </strong>
- </div>
- </div>
- <div class="row">
- <div class="col-12 text-muted">
- <span><t t-esc='record.location_id.string'/>: <t t-esc='record.location_id.value'/></span>
- </div>
- </div>
- <div class="row">
- <div class="col-12 text-muted">
- <span><t t-esc='record.theoretical_qty.string'/>: <t t-esc='record.theoretical_qty.value'/></span>
- </div>
- </div>
- <div class="row">
- <div class="col-12 text-muted">
- <span><t t-esc='record.product_qty.string'/>: <t t-esc='record.product_qty.value'/></span>
- </div>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- <p></p>
- <h3 class="oe_grey">Notes</h3>
- <ul class="oe_grey"><li>Inventory adjustments will be made by comparing the theoretical and the checked quantities.</li>
- <li>You can delete lines to ignore some products.</li>
- <li>If a product is not at the right place, set the checked quantity to 0 and create a new line with correct location.</li>
- </ul>
- </page>
- <page string="Inventory Adjustments" attrs="{'invisible': [('state', '!=', 'done')]}">
- <field name="move_ids">
- <tree decoration-muted="scrapped == True" string="Stock Moves">
- <field name="product_id"/>
- <field name="picking_id" invisible="1" />
- <field name="create_date" invisible="1" />
- <field name="date_expected" invisible="1" />
- <field name="scrapped" invisible="1"/>
- <field name="state" invisible="1"/>
- <field name="location_id" options="{'no_create': True}" string="From"/>
- <field name="location_dest_id" string="To"/>
- <field name="quantity_done"/>
- <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
- </tree>
- </field>
- </page>
- </notebook>
+ <div attrs="{'invisible': [('state', '=', 'done')]}">
+ <h3 class="oe_grey">Notes</h3>
+ <span class="oe_grey">Inventory Adjustment Lines are displayed:</span>
+ <ul class="oe_grey">
+ <li>In black if there is no difference between the quantity on hand and the counted quantity.</li>
+ <li>In <span class="text-info">blue</span> if there is a difference between the quantity on hand and the counted quantity.</li>
+ <li>In <span class="text-danger">red</span> if the quantity on hand has changed, it can be recomputed via the action menu at the top.</li>
+ </ul>
+ </div>
</sheet>
</form>
</field>
@@ -264,14 +214,26 @@
</field>
</record>
- <record id="action_inventory_line_tree" model="ir.actions.act_window">
- <field name="name">Inventory Lines</field>
- <field name="type">ir.actions.act_window</field>
- <field name="res_model">stock.inventory.line</field>
- <field name="view_type">form</field>
- <field name="view_mode">tree</field>
- <field name="view_id" ref="stock_inventory_line_tree2"/>
- <field name="domain">[('inventory_id', '=', active_id)]</field>
+ <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_inventory_form" id="menu_action_inventory_form" parent="menu_stock_warehouse_mgmt" sequence="30"/>
diff --git a/addons/stock/views/stock_move_line_views.xml b/addons/stock/views/stock_move_line_views.xml
index 37c7898a2e0..91cea4ca9b1 100644
--- a/addons/stock/views/stock_move_line_views.xml
+++ b/addons/stock/views/stock_move_line_views.xml
@@ -6,10 +6,12 @@
<field name="arch" type="xml">
<tree decoration-muted="state == 'cancel'" decoration-danger="(state not in ('cancel','done'))" string="Move Lines" create="0" default_order="date">
<field name="date"/>
- <field name="reference" string="Reference"/>
+ <field name="reference" string="Reference"
+ invisible="context.get('no_reference', False)"/>
<field name="product_id"/>
- <field name="location_id" options="{'no_create': True}"/>
- <field name="location_dest_id" options="{'no_create': True}"/>
+ <field name="lot_id" groups="stock.group_production_lot"/>
+ <field name="location_id"/>
+ <field name="location_dest_id"/>
<field name="qty_done" string="Quantity Done"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
<field name="state"/>
diff --git a/addons/stock/views/stock_template.xml b/addons/stock/views/stock_template.xml
index 3ca9f6783b2..7293de38e19 100644
--- a/addons/stock/views/stock_template.xml
+++ b/addons/stock/views/stock_template.xml
@@ -4,6 +4,8 @@
<template id="stock_assets_backend" name="mrp_workorder assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/stock/static/tests/route_tour.js"></script>
+ <script type="text/javascript" src="/stock/static/src/js/inventory_validate_button_controller.js"></script>
+ <script type="text/javascript" src="/stock/static/src/js/inventory_validate_button_view.js"></script>
<script type="text/javascript" src="/stock/static/src/js/stock_traceability_report_backend.js"></script>
<script type="text/javascript" src="/stock/static/src/js/stock_traceability_report_widgets.js"></script>
<link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/stock_traceability_report.scss"/>
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 3700cfaafa1..1deebbbb0db 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -19,6 +19,26 @@ class StockInventory(models.Model):
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_ref = self.env.ref('account.action_move_journal_line')
+ if not action_ref:
+ return False
+ action_data = action_ref.read()[0]
+ action_data['domain'] = [('stock_move_id.id', 'in', self.move_ids.ids)]
+ return action_data
@api.multi
def post_inventory(self):
@@ -649,6 +669,8 @@ class StockMove(models.Model):
ref = 'Revaluation of %s (negative inventory)' % ref
elif self.env.context.get('forced_quantity') is not None:
ref = 'Correction of %s (modification of past move)' % ref
+ if not ref and self.inventory_id:
+ ref = self.inventory_id.name
move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id)
if move_lines:
diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml
index 84dbf3f3a9a..17cf9a232c2 100644
--- a/addons/stock_account/views/stock_account_views.xml
+++ b/addons/stock_account/views/stock_account_views.xml
@@ -18,8 +18,14 @@
<field name="model">stock.inventory</field>
<field name="inherit_id" ref="stock.view_inventory_form"/>
<field name="arch" type="xml">
- <xpath expr="//field[@name='date']" position="after">
- <field name="accounting_date" attrs="{'readonly':[('state','!=', 'draft')]}" groups="base.group_no_one"/>
+ <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>