diff --git a/stock_reserve/__init__.py b/stock_reserve/__init__.py index 643bee7ab..18a8b8eae 100644 --- a/stock_reserve/__init__.py +++ b/stock_reserve/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ############################################################################## # # Author: Guewen Baconnier diff --git a/stock_reserve/__manifest__.py b/stock_reserve/__manifest__.py index f59d491d0..154250c6c 100644 --- a/stock_reserve/__manifest__.py +++ b/stock_reserve/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ############################################################################## # # Author: Guewen Baconnier @@ -19,25 +18,25 @@ # ############################################################################## -{'name': 'Stock Reservation', - 'summary': 'Stock reservations on products', - 'version': '10.0.1.0.0', - 'author': "Camptocamp,Odoo Community Association (OCA)", - 'category': 'Warehouse', - 'license': 'AGPL-3', - 'complexity': 'normal', - 'images': [], - 'website': "http://www.camptocamp.com", - 'depends': ['stock', - ], - 'demo': [], - 'data': ['view/stock_reserve.xml', - 'view/product.xml', - 'data/stock_data.xml', - 'security/ir.model.access.csv', - ], - 'auto_install': False, - 'test': ['test/stock_reserve.yml', - ], - 'installable': True, - } +{ + "name": "Stock Reservation", + "summary": "Stock reservations on products", + "version": "10.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "category": "Warehouse", + "license": "AGPL-3", + "complexity": "normal", + "images": [], + "website": "http://www.camptocamp.com", + "depends": ["stock",], + "demo": [], + "data": [ + "view/stock_reserve.xml", + "view/product.xml", + "data/stock_data.xml", + "security/ir.model.access.csv", + ], + "auto_install": False, + "test": ["test/stock_reserve.yml",], + "installable": True, +} diff --git a/stock_reserve/data/stock_data.xml b/stock_reserve/data/stock_data.xml index 589c6f4b5..9fc2ba4e1 100644 --- a/stock_reserve/data/stock_data.xml +++ b/stock_reserve/data/stock_data.xml @@ -1,26 +1,29 @@ - + - - - Reservation Stock - - - - - - -Release the stock reservation having a passed validity date - - -1 -days --1 - -stock.reservation -release_validity_exceeded -() - - - + + Release the stock reservation having a passed validity date + + + 1 + days + -1 + + stock.reservation + release_validity_exceeded + () + + diff --git a/stock_reserve/migrations/0.2/post-migration.py b/stock_reserve/migrations/0.2/post-migration.py index 40e439352..dc9cb32dc 100644 --- a/stock_reserve/migrations/0.2/post-migration.py +++ b/stock_reserve/migrations/0.2/post-migration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Author: Leonardo Pistone # Copyright 2015 Camptocamp SA # @@ -18,8 +17,9 @@ def migrate(cr, installed_version): """Update a wrong location that is no_update in XML.""" - if installed_version == '8.0.0.1': - cr.execute(''' + if installed_version == "8.0.0.1": + cr.execute( + """ UPDATE stock_location SET location_id = ( SELECT res_id @@ -39,4 +39,5 @@ def migrate(cr, installed_version): WHERE name = 'stock_location_company' AND module = 'stock' ); - ''') + """ + ) diff --git a/stock_reserve/model/__init__.py b/stock_reserve/model/__init__.py index 9adf1d54b..28b34087b 100644 --- a/stock_reserve/model/__init__.py +++ b/stock_reserve/model/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ############################################################################## # # Author: Guewen Baconnier diff --git a/stock_reserve/model/product.py b/stock_reserve/model/product.py index 868664e72..c07b55ce1 100644 --- a/stock_reserve/model/product.py +++ b/stock_reserve/model/product.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ############################################################################## # # Author: Guewen Baconnier @@ -19,59 +18,62 @@ # ############################################################################## -from odoo import models, fields, api +from odoo import api, fields, models class ProductTemplate(models.Model): - _inherit = 'product.template' + _inherit = "product.template" reservation_count = fields.Float( - compute='_compute_reservation_count', - string='# Sales') + compute="_compute_reservation_count", string="# Sales" + ) @api.multi def _compute_reservation_count(self): for product in self: product.reservation_count = sum( - product.product_variant_ids.mapped('reservation_count')) + product.product_variant_ids.mapped("reservation_count") + ) @api.multi def action_view_reservations(self): self.ensure_one() - ref = 'stock_reserve.action_stock_reservation_tree' - product_ids = self.mapped('product_variant_ids.id') + ref = "stock_reserve.action_stock_reservation_tree" + product_ids = self.mapped("product_variant_ids.id") action_dict = self.env.ref(ref).read()[0] - action_dict['domain'] = [('product_id', 'in', product_ids)] - action_dict['context'] = { - 'search_default_draft': 1, - 'search_default_reserved': 1 - } + action_dict["domain"] = [("product_id", "in", product_ids)] + action_dict["context"] = { + "search_default_draft": 1, + "search_default_reserved": 1, + } return action_dict class ProductProduct(models.Model): - _inherit = 'product.product' + _inherit = "product.product" reservation_count = fields.Float( - compute='_compute_reservation_count', - string='# Sales') + compute="_compute_reservation_count", string="# Sales" + ) @api.multi def _compute_reservation_count(self): for product in self: - domain = [('product_id', '=', product.id), - ('state', 'in', ['draft', 'assigned'])] - reservations = self.env['stock.reservation'].search(domain) - product.reservation_count = sum(reservations.mapped('product_qty')) + domain = [ + ("product_id", "=", product.id), + ("state", "in", ["draft", "assigned"]), + ] + reservations = self.env["stock.reservation"].search(domain) + product.reservation_count = sum(reservations.mapped("product_qty")) @api.multi def action_view_reservations(self): self.ensure_one() - ref = 'stock_reserve.action_stock_reservation_tree' + ref = "stock_reserve.action_stock_reservation_tree" action_dict = self.env.ref(ref).read()[0] - action_dict['domain'] = [('product_id', '=', self.id)] - action_dict['context'] = { - 'search_default_draft': 1, - 'search_default_reserved': 1 - } + action_dict["domain"] = [("product_id", "=", self.id)] + action_dict["context"] = { + "search_default_draft": 1, + "search_default_reserved": 1, + } return action_dict diff --git a/stock_reserve/model/stock_reserve.py b/stock_reserve/model/stock_reserve.py index 755dc8305..b197f4346 100644 --- a/stock_reserve/model/stock_reserve.py +++ b/stock_reserve/model/stock_reserve.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ############################################################################## # # Author: Guewen Baconnier @@ -19,7 +18,7 @@ # ############################################################################## -from odoo import models, fields, api +from odoo import api, fields, models from odoo.exceptions import except_orm from odoo.tools.translate import _ @@ -46,18 +45,20 @@ class StockReservation(models.Model): * date_validity (once passed, the reservation will be released) * note """ - _name = 'stock.reservation' - _description = 'Stock Reservation' - _inherits = {'stock.move': 'move_id'} + + _name = "stock.reservation" + _description = "Stock Reservation" + _inherits = {"stock.move": "move_id"} move_id = fields.Many2one( - 'stock.move', - 'Reservation Move', + "stock.move", + "Reservation Move", required=True, readonly=True, - ondelete='cascade', - index=True) - date_validity = fields.Date('Validity Date') + ondelete="cascade", + index=True, + ) + date_validity = fields.Date("Validity Date") @api.model def default_get(self, fields_list): @@ -74,14 +75,13 @@ class StockReservation(models.Model): # if there is 'location_id' field requested, ensure that # picking_type_id is also requested, because it is required # to compute location_id - if ('location_id' in fields_list and - 'picking_type_id' not in fields_list): - fields_list = fields_list + ['picking_type_id'] + if "location_id" in fields_list and "picking_type_id" not in fields_list: + fields_list = fields_list + ["picking_type_id"] res = super(StockReservation, self).default_get(fields_list) - if 'product_qty' in res: - del res['product_qty'] + if "product_qty" in res: + del res["product_qty"] # At this point picking_type_id and location_id # should be computed in default way: @@ -93,19 +93,20 @@ class StockReservation(models.Model): # # If picking_type_id is present and location_id is not, try to find # default value for location_id - if not res.get('picking_type_id', None): - res['picking_type_id'] = self._default_picking_type_id() + if not res.get("picking_type_id", None): + res["picking_type_id"] = self._default_picking_type_id() - picking_type_id = res.get('picking_type_id') - if picking_type_id and not res.get('location_id', False): - picking = self.env['stock.picking'].new( - {'picking_type_id': picking_type_id}) + picking_type_id = res.get("picking_type_id") + if picking_type_id and not res.get("location_id", False): + picking = self.env["stock.picking"].new( + {"picking_type_id": picking_type_id} + ) picking.onchange_picking_type() - res['location_id'] = picking.location_id.id - if 'location_dest_id' in fields_list: - res['location_dest_id'] = self._default_location_dest_id() - if 'product_uom_qty' in fields_list: - res['product_uom_qty'] = 1.0 + res["location_id"] = picking.location_id.id + if "location_dest_id" in fields_list: + res["location_dest_id"] = self._default_location_dest_id() + if "product_uom_qty" in fields_list: + res["product_uom_qty"] = 1.0 return res @api.model @@ -115,7 +116,7 @@ class StockReservation(models.Model): """ try: location = self.env.ref(ref, raise_if_not_found=True) - location.check_access_rule('read') + location.check_access_rule("read") location_id = location.id except (except_orm, ValueError): location_id = False @@ -123,12 +124,12 @@ class StockReservation(models.Model): @api.model def _default_picking_type_id(self): - ref = 'stock.picking_type_out' + ref = "stock.picking_type_out" return self.env.ref(ref, raise_if_not_found=False).id @api.model def _default_location_dest_id(self): - ref = 'stock_reserve.stock_location_reservation' + ref = "stock_reserve.stock_location_reservation" return self.get_location_from_ref(ref) @api.multi @@ -138,9 +139,9 @@ class StockReservation(models.Model): The reservation is done using the default UOM of the product. A date until which the product is reserved can be specified. """ - self.write({'date_expected': fields.Datetime.now()}) - self.mapped('move_id').action_confirm() - self.mapped('move_id.picking_id').action_assign() + self.write({"date_expected": fields.Datetime.now()}) + self.mapped("move_id").action_confirm() + self.mapped("move_id.picking_id").action_assign() return True @api.multi @@ -148,16 +149,18 @@ class StockReservation(models.Model): """ Release moves from reservation """ - self.mapped('move_id').action_cancel() + self.mapped("move_id").action_cancel() return True @api.model def release_validity_exceeded(self, ids=None): """ Release all the reservation having an exceeded validity date """ - domain = [('date_validity', '<', fields.date.today()), - ('state', '=', 'assigned')] + domain = [ + ("date_validity", "<", fields.date.today()), + ("state", "=", "assigned"), + ] if ids: - domain.append(('id', 'in', ids)) + domain.append(("id", "in", ids)) self.search(domain).release() return True @@ -167,18 +170,19 @@ class StockReservation(models.Model): self.release() return super(StockReservation, self).unlink() - @api.onchange('product_id') + @api.onchange("product_id") def _onchange_product_id(self): """ set product_uom and name from product onchange """ # save value before reading of self.move_id as this last one erase # product_id value - move = self.move_id or self.env['stock.move'].new( - {'product_id': self.product_id}) + move = self.move_id or self.env["stock.move"].new( + {"product_id": self.product_id} + ) move.onchange_product_id() self.name = move.name self.product_uom = move.product_uom - @api.onchange('product_uom_qty') + @api.onchange("product_uom_qty") def _onchange_quantity(self): """ On change of product quantity avoid negative quantities """ if not self.product_id or self.product_uom_qty <= 0.0: @@ -187,13 +191,12 @@ class StockReservation(models.Model): @api.multi def open_move(self): self.ensure_one() - action = self.env.ref('stock.stock_move_action') + action = self.env.ref("stock.stock_move_action") action_dict = action.read()[0] - action_dict['name'] = _('Reservation Move') + action_dict["name"] = _("Reservation Move") # open directly in the form view - view_id = self.env.ref('stock.view_move_form').id + view_id = self.env.ref("stock.view_move_form").id action_dict.update( - views=[(view_id, 'form')], - res_id=self.move_id.id, - ) + views=[(view_id, "form")], res_id=self.move_id.id, + ) return action_dict diff --git a/stock_reserve/test/stock_reserve.yml b/stock_reserve/test/stock_reserve.yml index 05d7ce4b8..99d21bb51 100644 --- a/stock_reserve/test/stock_reserve.yml +++ b/stock_reserve/test/stock_reserve.yml @@ -1,16 +1,12 @@ -- - I force recomputation of stock.location parent left/right -- - !python {model: stock.location}: +- I force recomputation of stock.location parent left/right +- ? !python {model: stock.location} # we need this because when running the tests at install time as is done on # Travis, the hook performing this operation for the new stock reservation # location is run after the test execution. This causes the stock level # computation to be wrong at the time the tests are run. - self._parent_store_compute() -- - I create a product to test the stock reservation -- - !record {model: product.product, id: product_sorbet}: + : self._parent_store_compute() +- I create a product to test the stock reservation +- !record {model: product.product, id: product_sorbet}: default_code: 001SORBET name: Sorbet type: product @@ -19,137 +15,96 @@ standard_price: 70.0 uom_id: product.product_uom_kgm uom_po_id: product.product_uom_kgm -- - I create a stock orderpoint for the product -- - !record {model: stock.warehouse.orderpoint, id: sorbet_orderpoint}: +- I create a stock orderpoint for the product +- !record {model: stock.warehouse.orderpoint, id: sorbet_orderpoint}: warehouse_id: stock.warehouse0 location_id: stock.stock_location_stock product_id: product_sorbet product_uom: product.product_uom_kgm product_min_qty: 4.0 product_max_qty: 15.0 -- - I update the current stock of the Sorbet with 10 kgm -- - !record {model: stock.change.product.qty, id: change_qty}: +- I update the current stock of the Sorbet with 10 kgm +- !record {model: stock.change.product.qty, id: change_qty}: new_quantity: 10 product_id: product_sorbet -- - !python {model: stock.change.product.qty}: | +- !python {model: stock.change.product.qty}: | self.browse(ref('change_qty')).change_product_qty() -- - I check Virtual stock of Sorbet after update stock. -- - !python {model: product.product, id: product_sorbet}: | +- I check Virtual stock of Sorbet after update stock. +- !python {model: product.product, id: product_sorbet}: | assert self.virtual_available == 10, "Stock is not updated." -- - I create a stock reservation for 6 kgm -- - !record {model: stock.reservation, id: reserv_sorbet1}: +- I create a stock reservation for 6 kgm +- !record {model: stock.reservation, id: reserv_sorbet1}: product_id: product_sorbet product_uom_qty: 6.0 product_uom: product.product_uom_kgm name: reserve 6 kg of sorbet for test -- - I confirm the reservation -- - !python {model: stock.reservation}: | +- I confirm the reservation +- !python {model: stock.reservation}: | self.browse(ref('reserv_sorbet1')).reserve() -- - I create a stock reservation for 500g -- - !record {model: stock.reservation, id: reserv_sorbet2}: +- I create a stock reservation for 500g +- !record {model: stock.reservation, id: reserv_sorbet2}: product_id: product_sorbet product_uom_qty: 500 product_uom: product.product_uom_gram name: reserve 500g of sorbet for test -- - I confirm the reservation -- - !python {model: stock.reservation, id: reserv_sorbet2}: | +- I confirm the reservation +- !python {model: stock.reservation, id: reserv_sorbet2}: | self.reserve() -- - I check the reserved amount of the product and the template -- - !python {model: product.product, id: product_sorbet}: | +- I check the reserved amount of the product and the template +- !python {model: product.product, id: product_sorbet}: | assert 6.5 == self.reservation_count assert 6.5 == self.product_tmpl_id.reservation_count -- - Then the reservation should be assigned and have reserved a quant -- - !python {model: stock.reservation, id: reserv_sorbet2}: | +- Then the reservation should be assigned and have reserved a quant +- !python {model: stock.reservation, id: reserv_sorbet2}: | assert 'assigned' == self.state assert 1 == len(self.reserved_quant_ids) -- - I check Virtual stock of Sorbet after update reservation -- - !python {model: product.product, id: product_sorbet}: | +- I check Virtual stock of Sorbet after update reservation +- !python {model: product.product, id: product_sorbet}: | assert 3.5 == self.virtual_available -- - I run the scheduler -- - !python {model: procurement.order}: | +- I run the scheduler +- !python {model: procurement.order}: | self.run_scheduler() -- - The procurement linked to the orderpoint must be in exception (no routes configured) -- - !python {model: stock.warehouse.orderpoint}: | +- The procurement linked to the orderpoint must be in exception (no routes configured) +- !python {model: stock.warehouse.orderpoint}: | orderpoint = self.browse(ref('stock_reserve.sorbet_orderpoint')) assert orderpoint.procurement_ids[0].state == 'exception', 'procurement must be in exception as there is no rule for procurement' import math assert orderpoint.procurement_ids[0].product_qty == math.ceil(15 - 3.5), 'wrong product qty ordered' -- - I release the reservation -- - !python {model: stock.reservation}: | +- I release the reservation +- !python {model: stock.reservation}: | self.browse(ref('reserv_sorbet1')).release() -- - I check Virtual stock of Sorbet after update reservation -- - !python {model: product.product}: | +- I check Virtual stock of Sorbet after update reservation +- !python {model: product.product}: | product = self.browse(ref('stock_reserve.product_sorbet')) assert product.virtual_available == 9.5, "Stock is not updated." -- - I set the validity of the second reservation to yesterday -- - !python {model: stock.reservation}: | +- I set the validity of the second reservation to yesterday +- !python {model: stock.reservation}: | import datetime from odoo.tools import DEFAULT_SERVER_DATE_FORMAT yesterday = datetime.date.today() - datetime.timedelta(days=1) yesterday = yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) self.browse(ref('reserv_sorbet2')).write({'date_validity': yesterday}) -- - I call the function releasing expired reservations -- - !python {model: stock.reservation}: | +- I call the function releasing expired reservations +- !python {model: stock.reservation}: | self.release_validity_exceeded() -- - I check Virtual stock of Sorbet after update reservation -- - !python {model: product.product}: | +- I check Virtual stock of Sorbet after update reservation +- !python {model: product.product}: | product = self.browse(ref('stock_reserve.product_sorbet')) product.refresh() assert product.virtual_available == 10.0, "Stock is not updated." -- - I create a stock reservation for 3 kgm -- - !record {model: stock.reservation, id: reserv_sorbet3}: +- I create a stock reservation for 3 kgm +- !record {model: stock.reservation, id: reserv_sorbet3}: product_id: product_sorbet product_uom_qty: 3.0 product_uom: product.product_uom_kgm name: reserve 3 kg of sorbet for test (release on unlink) -- - I confirm the reservation -- - !python {model: stock.reservation}: | +- I confirm the reservation +- !python {model: stock.reservation}: | self.browse(ref('reserv_sorbet3')).reserve() -- - I press the open_move button on reservation and test result -- - !python {model: stock.reservation}: | +- I press the open_move button on reservation and test result +- !python {model: stock.reservation}: | reserv = self.browse(ref('reserv_sorbet3')) move = reserv.move_id action_dict = reserv.open_move() @@ -158,10 +113,8 @@ assert action_dict['id'] == ref('stock.stock_move_action'), "action not correct" assert action_dict['views'][0][0] == ref('stock.view_move_form'), "action view not correct" -- - I press button 'action_view_reservations' on product variant and test result -- - !python {model: product.product}: | +- I press button 'action_view_reservations' on product variant and test result +- !python {model: product.product}: | product = self.browse(ref('product_sorbet')) action_dict = product.action_view_reservations() assert action_dict['res_model'] == 'stock.reservation', "action model is not 'stock.move'" @@ -171,10 +124,8 @@ assert action_dict['context']['search_default_draft'] == 1, "wrong context" assert action_dict['context']['search_default_reserved'] == 1, "wrong context" -- - I press button 'action_view_reservations' on product template and test result -- - !python {model: product.template}: | +- I press button 'action_view_reservations' on product template and test result +- !python {model: product.template}: | product = self.env['product.product'].browse(ref('product_sorbet')) product_tmpl = product.product_tmpl_id product_ids = product_tmpl.mapped('product_variant_ids.id') @@ -188,10 +139,8 @@ assert action_dict['context']['search_default_draft'] == 1, "wrong context" assert action_dict['context']['search_default_reserved'] == 1, "wrong context" -- - I unlink the reservation -- - !python {model: stock.reservation}: | +- I unlink the reservation +- !python {model: stock.reservation}: | reserv = self.browse(ref('reserv_sorbet3')) move = reserv.move_id reserv.unlink() diff --git a/stock_reserve/view/product.xml b/stock_reserve/view/product.xml index 118cba305..17794c459 100644 --- a/stock_reserve/view/product.xml +++ b/stock_reserve/view/product.xml @@ -1,31 +1,47 @@ - + - - - product.template.reservation.button - product.template - + + product.template.reservation.button + product.template + - - - - - - - - - - product.template.reservation.button - product.product - - - - - - - - - + + + + + + + + + product.template.reservation.button + product.product + + + + + + + + + diff --git a/stock_reserve/view/stock_reserve.xml b/stock_reserve/view/stock_reserve.xml index 760ac9dd9..8ff8c4910 100644 --- a/stock_reserve/view/stock_reserve.xml +++ b/stock_reserve/view/stock_reserve.xml @@ -1,140 +1,192 @@ - + - - stock.reservation.form - stock.reservation - - - - + stock.reservation.form + stock.reservation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stock.reservation.tree + stock.reservation + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + icon="gtk-undo" + states="assigned,confirmed,done" + /> + + + + + stock.reservation.search + stock.reservation + + + + + + + + + + + + + + - - - - - - - stock.reservation.tree - stock.reservation - - - - - - - - - - - - - - - - - - - stock.reservation.search - stock.reservation - - - - - - - - - - - - - - - - - - - - - Stock Reservations - stock.reservation - ir.actions.act_window - - - {'search_default_draft': 1, + + + + + Stock Reservations + stock.reservation + ir.actions.act_window + + + {'search_default_draft': 1, 'search_default_reserved': 1, 'search_default_groupby_product': 1} - - + + Click to create a stock reservation. - + + This menu allow you to prepare and reserve some quantities of products. - - - - + + +
+ + Click to create a stock reservation. - + + This menu allow you to prepare and reserve some quantities of products. - -
Click to create a stock reservation. -
+
This menu allow you to prepare and reserve some quantities of products.