diff options
| author | svs-odoo <svs@odoo.com> | 2019-03-12 12:22:41 +0000 |
|---|---|---|
| committer | svs-odoo <svs@odoo.com> | 2019-05-29 14:03:29 +0000 |
| commit | cc9324f74084760da3e49f069e3ef62f0d4280d7 (patch) | |
| tree | 5f40efdd92de7cddb73c91a06f8a0de4ffe08dc4 /addons | |
| parent | da33eb36b80d1b2a0004c28cf20d6b0263bcb643 (diff) | |
[IMP] *: usability of Inventory Adjustment
Change the use of Inventory Adjustment, notable changes are:
- Removed filter field: When user creates a new inventory
adjustment, he can set one or multiple locations and/or products.
An inventory adjustment will worry only about defined
locations/products, but if neither product or location was set,
it'll manage all stock.
- Inventory Adjustment Lines have their own view instead of be
listed on the Inventory Adjustment form view.
Inventory adjustment lines have new color legend:
- Red: The quantity is outdated.
- Blue: There is difference between the on hand quantity and the
counted quantity.
New inventory lines created by the user are written in bold.
- User can't modify already existing inventory lines, except for the
counted quantity.
- When an inventory line is outdated, the user has the possibility
to select and update it, that'll recompute the on hand quantity.
- When an inventory adjustment is validated, it will take in account
only the difference between the theoretical quantity ('On Hand
Quantity') and the counted quantity to adjust the quants.
- When an inventory adjustment is canceled, it will keep its
inventory lines and won't regenerate them when re-started.
- When an Inventory Adjustment generates Account Moves, user can now
find them in a stat button in the Inventory Adjustment form view.
Also, made some changes in demo data to match new requirement.
Task #1935921
Diffstat (limited to 'addons')
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 < 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 < 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<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> |
