From 424f7cd8f29772881b550dbe8cad539bba4758bc Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Mon, 1 Sep 2014 15:24:57 +0200 Subject: [PATCH] reactivate module stock_reserve --- stock_reserve/__init__.py | 22 +++ stock_reserve/__openerp__.py | 65 +++++++ stock_reserve/data/stock_data.xml | 26 +++ stock_reserve/model/__init__.py | 23 +++ stock_reserve/model/product.py | 84 +++++++++ stock_reserve/model/stock_reserve.py | 197 +++++++++++++++++++++ stock_reserve/security/ir.model.access.csv | 3 + stock_reserve/test/stock_reserve.yml | 58 ++++++ stock_reserve/view/product.xml | 33 ++++ stock_reserve/view/stock_reserve.xml | 135 ++++++++++++++ 10 files changed, 646 insertions(+) create mode 100644 stock_reserve/__init__.py create mode 100644 stock_reserve/__openerp__.py create mode 100644 stock_reserve/data/stock_data.xml create mode 100644 stock_reserve/model/__init__.py create mode 100644 stock_reserve/model/product.py create mode 100644 stock_reserve/model/stock_reserve.py create mode 100644 stock_reserve/security/ir.model.access.csv create mode 100644 stock_reserve/test/stock_reserve.yml create mode 100644 stock_reserve/view/product.xml create mode 100644 stock_reserve/view/stock_reserve.xml diff --git a/stock_reserve/__init__.py b/stock_reserve/__init__.py new file mode 100644 index 000000000..643bee7ab --- /dev/null +++ b/stock_reserve/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import model diff --git a/stock_reserve/__openerp__.py b/stock_reserve/__openerp__.py new file mode 100644 index 000000000..d1114f1fc --- /dev/null +++ b/stock_reserve/__openerp__.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{'name': 'Stock Reservation', + 'summary': 'Stock reservations on products', + 'version': '0.1', + 'author': 'Camptocamp', + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'complexity': 'normal', + 'images': [], + 'website': "http://www.camptocamp.com", + 'description': """ +Stock Reservation +================= + +Allows to create stock reservations on products. + +Each reservation can have a validity date, once passed, the reservation +is automatically lifted. + +The reserved products are substracted from the virtual stock. It means +that if you reserved a quantity of products which bring the virtual +stock below the minimum, the orderpoint will be triggered and new +purchase orders will be generated. It also implies that the max may be +exceeded if the reservations are canceled. + +Contributors +------------ + +* Guewen Baconnier +* Yannick Vaucher + +""", + '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 new file mode 100644 index 000000000..be6e44224 --- /dev/null +++ b/stock_reserve/data/stock_data.xml @@ -0,0 +1,26 @@ + + + + + Reservation Stock + + + + + + + Release the stock reservation having a passed validity date + + + 1 + days + -1 + + stock.reservation + release_validity_exceeded + () + + + + diff --git a/stock_reserve/model/__init__.py b/stock_reserve/model/__init__.py new file mode 100644 index 000000000..9adf1d54b --- /dev/null +++ b/stock_reserve/model/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import stock_reserve +from . import product diff --git a/stock_reserve/model/product.py b/stock_reserve/model/product.py new file mode 100644 index 000000000..1349a660b --- /dev/null +++ b/stock_reserve/model/product.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + reservation_count = fields.Integer( + compute='_reservation_count', + string='# Sales') + + @api.multi + def _reservation_count(self): + StockReservation = self.env['stock.reservation'] + product_ids = self._get_products() + domain = [('product_id', 'in', product_ids), + ('state', 'in', ['draft', 'assigned'])] + reservations = StockReservation.search(domain) + self.reservation_count = sum(reserv.product_uom_qty + for reserv in reservations) + + @api.multi + def action_view_reservations(self): + assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids + ref = 'stock_reserve.action_stock_reservation_tree' + product_ids = self._get_products() + action_dict = self._get_act_window_dict(ref) + action_dict['domain'] = ("[('product_id','in',[" + + ','.join(map(str, product_ids)) + "])]") + action_dict['context'] = { + 'search_default_draft': 1, + 'search_default_reserved': 1 + } + return action_dict + +class ProductProduct(models.Model): + _inherit = 'product.product' + + reservation_count = fields.Integer( + compute='_reservation_count', + string='# Sales') + + @api.multi + def _reservation_count(self): + StockReservation = self.env['stock.reservation'] + product_id = self._ids[0] + domain = [('product_id', '=', product_id), + ('state', 'in', ['draft', 'assigned'])] + reservations = StockReservation.search(domain) + self.reservation_count = sum(reserv.product_uom_qty + for reserv in reservations) + + @api.multi + def action_view_reservations(self): + assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids + ref = 'stock_reserve.action_stock_reservation_tree' + product_id = self._ids[0] + action_dict = self.product_tmpl_id._get_act_window_dict(ref) + action_dict['domain'] = ("[('product_id','='," + str(product_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 new file mode 100644 index 000000000..aa5e08844 --- /dev/null +++ b/stock_reserve/model/stock_reserve.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2013 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api +from openerp.exceptions import except_orm +from openerp.tools.translate import _ + + +class StockReservation(models.Model): + """ Allow to reserve products. + + The fields mandatory for the creation of a reservation are: + + * product_id + * product_uom_qty + * product_uom + * name + + The following fields are required but have default values that you may + want to override: + + * company_id + * location_id + * dest_location_id + + Optionally, you may be interested to define: + + * date_validity (once passed, the reservation will be released) + * note + """ + _name = 'stock.reservation' + _description = 'Stock Reservation' + _inherits = {'stock.move': 'move_id'} + + move_id = fields.Many2one( + 'stock.move', + 'Reservation Move', + required=True, + readonly=True, + ondelete='cascade', + select=1) + date_validity = fields.Date('Validity Date') + + @api.model + def default_get(self, fields_list): + """ + Ensure default value of computed field `product_qty` is not set + as it would raise an error + """ + res = super(StockReservation, self).default_get(fields_list) + if 'product_qty' in res: + del res['product_qty'] + return res + + @api.model + def get_location_from_ref(self, ref): + """ Get a location from a xmlid if allowed + :param ref: tuple (module, xmlid) + """ + data_obj = self.env['ir.model.data'] + try: + location = data_obj.xmlid_to_object(ref, raise_if_not_found=True) + location.check_access_rule('read') + location_id = location.id + except (except_orm, ValueError): + location_id = False + return location_id + + @api.model + def _default_picking_type_id(self): + """ Search for an internal picking type + """ + move_obj = self.env['stock.move'] + type_obj = self.env['stock.picking.type'] + + types = type_obj.search([('code', '=', 'internal')], limit=1) + if types: + return types[0].id + return False + + @api.model + def _default_location_id(self): + move_obj = self.env['stock.move'] + picking_type_id = self._default_picking_type_id() + return (move_obj + .with_context(default_picking_type_id=picking_type_id) + ._default_location_source()) + + @api.model + def _default_location_dest_id(self): + ref = 'stock_reserve.stock_location_reservation' + return self.get_location_from_ref(ref) + + _defaults = { + 'picking_type_id': _default_picking_type_id, + 'location_id': _default_location_id, + 'location_dest_id': _default_location_dest_id, + 'product_uom_qty': 1.0, + } + + @api.multi + def reserve(self): + """ Confirm a reservation + + The reservation is done using the default UOM of the product. + A date until which the product is reserved can be specified. + """ + move_recs = self.move_id + move_recs.date_expected = fields.Datetime.now() + move_recs.action_confirm() + move_recs.force_assign() + return True + + @api.multi + def release(self): + """ + Releas moves from reservation + """ + move_recs = self.move_id + move_recs.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')] + if ids: + domain.append(('id', 'in', ids)) + reserv_ids = self.search(domain) + self.release(reserv_ids) + return True + + @api.multi + def unlink(self): + """ Release the reservation before the unlink """ + self.release() + return super(StockReservation, self).unlink() + + @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 + product = self.product_id + # WARNING this gettattr erase self.product_id + move = self.move_id + result = move.onchange_product_id( + prod_id=product.id, loc_id=False, loc_dest_id=False, + partner_id=False) + if result.get('value'): + vals = result['value'] + # only keep the existing fields on the view + self.name = vals.get('name') + self.product_uom = vals.get('product_uom') + # repeat assignation of product_id so we don't loose it + self.product_id = product.id + + @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: + self.product_uom_qty = 0.0 + + @api.multi + def open_move(self): + assert len(self._ids) == 1, "1 ID expected, got %r" % self._ids + reserv = self[0].move_id + data_obj = self.env['ir.model.data'] + ref_form2 = 'stock.action_move_form2' + action = data_obj.xmlid_to_object(ref_form2) + action_dict = action.read() + action_dict['name'] = _('Reservation Move') + # open directly in the form view + ref_form = 'stock.view_move_form' + view_id = data_obj.xmlid_to_res_id(ref_form) + action['views'] = [(view_id, 'form')] + action['res_id'] = reserv['move_id'] + return action diff --git a/stock_reserve/security/ir.model.access.csv b/stock_reserve/security/ir.model.access.csv new file mode 100644 index 000000000..c48a8eefa --- /dev/null +++ b/stock_reserve/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_reservation_manager,stock.reservation manager,model_stock_reservation,stock.group_stock_manager,1,1,1,1 +access_stock_reservation_user,stock.reservation user,model_stock_reservation,stock.group_stock_user,1,1,1,0 diff --git a/stock_reserve/test/stock_reserve.yml b/stock_reserve/test/stock_reserve.yml new file mode 100644 index 000000000..ccced7023 --- /dev/null +++ b/stock_reserve/test/stock_reserve.yml @@ -0,0 +1,58 @@ +- + I create a product to test the stock reservation +- + !record {model: product.product, id: product_sorbet}: + default_code: 001SORBET + name: Sorbet + type: product + categ_id: product.product_category_1 + list_price: 100.0 + standard_price: 70.0 + uom_id: product.product_uom_kgm + uom_po_id: product.product_uom_kgm +- + 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}: | + context['active_id'] = ref('stock_reserve.product_sorbet') + self.change_product_qty(cr, uid, [ref('change_qty')], context=context) +- + I check Virtual stock of Sorbet after update stock. +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) + assert product.virtual_available == 10, "Stock is not updated." +- + I create a stock reservation for 5 kgm +- + !record {model: stock.reservation, id: reserv_sorbet1}: + product_id: product_sorbet + product_uom_qty: 5.0 + product_uom: product.product_uom_kgm + name: reserve 5 kgm of sorbet for test +- + I confirm the reservation +- + !python {model: stock.reservation}: | + self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context) +- + I check Virtual stock of Sorbet after update reservation +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) + assert product.virtual_available == 5, "Stock is not updated." +- + I release the reservation +- + !python {model: stock.reservation}: | + self.release(cr, uid, [ref('reserv_sorbet1')], context=context) +- + I check Virtual stock of Sorbet after update reservation +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) + assert product.virtual_available == 10, "Stock is not updated." diff --git a/stock_reserve/view/product.xml b/stock_reserve/view/product.xml new file mode 100644 index 000000000..e4e1dc3fb --- /dev/null +++ b/stock_reserve/view/product.xml @@ -0,0 +1,33 @@ + + + + + + product.template.reservation.button + product.template + + + + + + + + + + product.template.reservation.button + product.product + + + + + + + + + diff --git a/stock_reserve/view/stock_reserve.xml b/stock_reserve/view/stock_reserve.xml new file mode 100644 index 000000000..7ee9cbdd3 --- /dev/null +++ b/stock_reserve/view/stock_reserve.xml @@ -0,0 +1,135 @@ + + + + + stock.reservation.form + stock.reservation + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + + stock.reservation.tree + stock.reservation + + + + + + + + + +