From d6fbbf0d963c18569b71ae6d9ac959b1f570529f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 5 Sep 2013 14:06:46 +0200 Subject: [PATCH] [ADD] started a generic stock reservation module (stock_reserve), it will serve as a basis for the sale pre-book --- stock_reserve/__init__.py | 22 +++ stock_reserve/__openerp__.py | 58 +++++++ stock_reserve/data/stock_data.xml | 26 +++ stock_reserve/model/__init__.py | 23 +++ stock_reserve/model/product.py | 40 +++++ stock_reserve/model/stock_reserve.py | 177 +++++++++++++++++++++ stock_reserve/security/ir.model.access.csv | 3 + stock_reserve/test/stock_reserve.yml | 63 ++++++++ stock_reserve/view/product.xml | 19 +++ stock_reserve/view/stock_reserve.xml | 140 ++++++++++++++++ 10 files changed, 571 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..61684f3c8 --- /dev/null +++ b/stock_reserve/__openerp__.py @@ -0,0 +1,58 @@ +# -*- 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 Reserve', + 'version': '0.1', + 'author': 'Camptocamp', + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'complexity': 'normal', + 'images': [], + 'website': "http://www.camptocamp.com", + 'description': """ +Stock Reserve +============= + +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. + +""", + '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..45b7f28aa --- /dev/null +++ b/stock_reserve/model/product.py @@ -0,0 +1,40 @@ +# -*- 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.osv import orm, fields + + +class product_product(orm.Model): + _inherit = 'product.product' + + def open_stock_reservation(self, cr, uid, ids, context=None): + assert len(ids) == 1, "Expected 1 ID, got %r" % ids + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + get_ref = mod_obj.get_object_reference + __, action_id = get_ref(cr, uid, 'stock_reserve', + 'action_stock_reservation') + action = act_obj.read(cr, uid, action_id, context=context) + action['context'] = {'search_default_draft': 1, + 'search_default_reserved': 1, + 'default_product_id': ids[0], + 'search_default_product_id': ids[0]} + return action diff --git a/stock_reserve/model/stock_reserve.py b/stock_reserve/model/stock_reserve.py new file mode 100644 index 000000000..6466ac233 --- /dev/null +++ b/stock_reserve/model/stock_reserve.py @@ -0,0 +1,177 @@ +# -*- 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.osv import orm, fields +from openerp.tools.translate import _ + + +class stock_reservation(orm.Model): + """ Allow to reserve products. + + The fields mandatory for the creation of a reservation are: + + * product_id + * product_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'} + + _columns = { + 'move_id': fields.many2one('stock.move', + 'Reservation Move', + required=True, + readonly=True, + ondelete='cascade', + select=1), + 'date_validity': fields.date('Validity Date'), + } + + def get_location_from_ref(self, cr, uid, ref, context=None): + """ Get a location from a xmlid if allowed + :param ref: tuple (module, xmlid) + """ + location_obj = self.pool.get('stock.location') + data_obj = self.pool.get('ir.model.data') + get_ref = data_obj.get_object_reference + try: + __, location_id = get_ref(cr, uid, *ref) + location_obj.check_access_rule(cr, uid, [location_id], + 'read', context=context) + except (orm.except_orm, ValueError): + location_id = False + return location_id + + def _default_location_id(self, cr, uid, context=None): + if context is None: + context = {} + move_obj = self.pool.get('stock.move') + context['picking_type'] = 'internal' + return move_obj._default_location_source(cr, uid, context=context) + + def _default_location_dest_id(self, cr, uid, context=None): + ref = ('stock_reserve', 'stock_location_reservation') + return self.get_location_from_ref(cr, uid, ref, context=context) + + _defaults = { + 'type': 'internal', + 'location_id': _default_location_id, + 'location_dest_id': _default_location_dest_id, + 'product_qty': 1.0, + } + + def reserve(self, cr, uid, ids, context=None): + """ 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_obj = self.pool.get('stock.move') + reservations = self.browse(cr, uid, ids, context=context) + move_ids = [reserv.move_id.id for reserv in reservations] + move_obj.write(cr, uid, move_ids, + {'date_expected': fields.datetime.now()}, + context=context) + move_obj.action_confirm(cr, uid, move_ids, context=context) + move_obj.force_assign(cr, uid, move_ids, context=context) + return True + + def release(self, cr, uid, ids, context=None): + if isinstance(ids, (int, long)): + ids = [ids] + reservations = self.read(cr, uid, ids, ['move_id'], + context=context, load='_classic_write') + move_obj = self.pool.get('stock.move') + move_ids = [reserv['move_id'] for reserv in reservations] + move_obj.action_cancel(cr, uid, move_ids, context=context) + return True + + def release_validity_exceeded(self, cr, uid, ids=None, context=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(cr, uid, domain, context=context) + self.release(cr, uid, reserv_ids, context=context) + return True + + def unlink(self, cr, uid, ids, context=None): + """ Release the reservation before the unlink """ + self.release(cr, uid, ids, context=context) + return super(stock_reservation, self).unlink(cr, uid, ids, + context=context) + + def onchange_product_id(self, cr, uid, ids, product_id=False, context=None): + move_obj = self.pool.get('stock.move') + if ids: + reserv = self.read(cr, uid, ids, ['move_id'], context=context, + load='_classic_write') + move_ids = [rv['move_id'] for rv in reserv] + else: + move_ids = [] + result = move_obj.onchange_product_id( + cr, uid, move_ids, 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 + keep = ('product_uom', 'name') + result['value'] = dict((key, value) for key, value in + result['value'].iteritems() if + key in keep) + return result + + def onchange_quantity(self, cr, uid, ids, product_id, product_qty, context=None): + """ On change of product quantity avoid negative quantities """ + if not product_id or product_qty <= 0.0: + return {'value': {'product_qty': 0.0}} + return {} + + def open_move(self, cr, uid, ids, context=None): + assert len(ids) == 1, "1 ID expected, got %r" % ids + reserv = self.read(cr, uid, ids[0], ['move_id'], context=context, + load='_classic_write') + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + get_ref = mod_obj.get_object_reference + __, action_id = get_ref(cr, uid, 'stock', 'action_move_form2') + action = act_obj.read(cr, uid, action_id, context=context) + action['name'] = _('Reservation Move') + # open directly in the form view + __, view_id = get_ref(cr, uid, 'stock', 'view_move_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..5cd92e29f --- /dev/null +++ b/stock_reserve/test/stock_reserve.yml @@ -0,0 +1,63 @@ +- + 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 + procure_method: make_to_stock + valuation: real_time + cost_method: average + property_stock_account_input: account.o_expense + property_stock_account_output: account.o_income +- + 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_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..9ff624dc1 --- /dev/null +++ b/stock_reserve/view/product.xml @@ -0,0 +1,19 @@ + + + + + + product.product.form.reserve + product.product + + + +