From 011c9c8ee9bad7cecb06e4a56bf279e0d8b3afc9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 5 Sep 2013 14:06:46 +0200 Subject: [PATCH 01/27] [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 | 47 ++++++++++ stock_reserve/model/__init__.py | 22 +++++ stock_reserve/model/stock_reserve.py | 124 +++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 stock_reserve/__init__.py create mode 100644 stock_reserve/__openerp__.py create mode 100644 stock_reserve/model/__init__.py create mode 100644 stock_reserve/model/stock_reserve.py 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..8b3d84116 --- /dev/null +++ b/stock_reserve/__openerp__.py @@ -0,0 +1,47 @@ +# -*- 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 reservation on a product or a selection of +products. The reservations can be monitored and lifter on the product +view. Each reservation can have a validity date, once reached, the +reservation is automatically lifted. + +""", + 'depends': ['stock', + ], + 'demo': [], + 'data': [], + 'auto_install': False, + 'test': [], + 'installable': True, + } diff --git a/stock_reserve/model/__init__.py b/stock_reserve/model/__init__.py new file mode 100644 index 000000000..7202763ed --- /dev/null +++ b/stock_reserve/model/__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 stock_reserve diff --git a/stock_reserve/model/stock_reserve.py b/stock_reserve/model/stock_reserve.py new file mode 100644 index 000000000..b045a5ff0 --- /dev/null +++ b/stock_reserve/model/stock_reserve.py @@ -0,0 +1,124 @@ +# -*- 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 datetime import datetime + +from openerp.osv import orm, fields +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FMT + + +class stock_reservation(orm.Model): + """ Allow to reserve products. + + A stock reservation is basically a stock move, + but the reservation is handled by this model using + a ``_inherits``. + """ + _name = 'stock.reservation' + _description = 'Stock Reservation' + _inherits = {'stock.move': 'move_id'} + + _columns = { + 'move_id': fields.many2one('stock.move', + 'Move', + readonly=True, + required=True, + ondelete='cascade', + select=1), + 'date_validity': fields.datetime('Validity Date'), + } + + def _prepare_reserve(self, cr, uid, product_id, quantity, location_id, + date_validity=None, context=None): + product_obj = self.pool.get('product.product') + product = product_obj.browse(cr, uid, product_id, context=context) + location_dest_id = self._get_reservation_location( + cr, uid, product_id, context=context) + vals = {'name': 'Reservation', # sequence? + 'product_id': product_id, + 'product_qty': quantity, + 'product_uom': product.uom_id.id, + 'location_id': location_id, + 'location_dest_id': location_dest_id, + 'price_unit': product.standard_price or 0.0, + } + if date_validity: + vals.update({'date_validity': date_validity, + 'date': date_validity, + 'date_expected': date_validity, + }) + else: + today = datetime.now().strftime(DT_FMT) + vals.update({'date': today, + 'date_expected': today, + }) + return vals + + def _get_reservation_location(self, cr, uid, product_id, context=None): + """ Returns the appropriate destination location to + reserve a product + """ + return 1 + + def reserve(self, cr, uid, product_id, quantity, location_id, + date_validity=None, context=None): + """ Reserve a product. + + The reservation is done using the default UOM of the product. + A date until which the product is reserved can be specified. + + :param product_id: id of the product to reserve + :param quantity: quantity of products to reserve + :param location_id: source location for the reservation + :param date_validity: optional datetime until which the reservation + is valid + :returns: id of the ``stock.reservation`` created + """ + vals = self._prepare_reserve(cr, uid, product_id, quantity, + location_id, + date_validity=date_validity, + context=context) + reservation_id = self.create(cr, uid, vals, context=context) + move_id = self.read(cr, uid, + reservation_id, + ['move_id'], + context=context, + load='_classic_write')['move_id'] + move_obj = self.pool.get('stock.move') + move_obj.action_confirm(cr, uid, [move_id], context=context) + # TODO: if no quantity in the location, it will stay 'confirmed' + # after action_confirm(), how to handle that? + move_obj.action_assign(cr, uid, [move_id]) + return reservation_id + + def release(self, cr, uid, ids, context=None): + """ Release a reservation """ + return self.unlink(cr, uid, ids, context=context) + + def unlink(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 super(stock_reservation, self).unlink(cr, uid, ids, context=context) From e8a2363ea9e65f77991125f426e6c90fffa8ca4a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 5 Sep 2013 14:07:06 +0200 Subject: [PATCH 02/27] [IMP] stock_reserve: add views, now able to reserve and release a move, added a default 'Reservation Stock' location --- stock_reserve/__openerp__.py | 6 +- stock_reserve/data/stock_data.xml | 9 ++ stock_reserve/model/stock_reserve.py | 115 +++++++++++++------------- stock_reserve/view/stock_reserve.xml | 118 +++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 62 deletions(-) create mode 100644 stock_reserve/data/stock_data.xml create mode 100644 stock_reserve/view/stock_reserve.xml diff --git a/stock_reserve/__openerp__.py b/stock_reserve/__openerp__.py index 8b3d84116..dd75c1a20 100644 --- a/stock_reserve/__openerp__.py +++ b/stock_reserve/__openerp__.py @@ -32,7 +32,7 @@ Stock Reserve ============= Allows to create stock reservation on a product or a selection of -products. The reservations can be monitored and lifter on the product +products. The reservations can be monitored and lifted on the product view. Each reservation can have a validity date, once reached, the reservation is automatically lifted. @@ -40,7 +40,9 @@ reservation is automatically lifted. 'depends': ['stock', ], 'demo': [], - 'data': [], + 'data': ['view/stock_reserve.xml', + 'data/stock_data.xml', + ], 'auto_install': False, 'test': [], 'installable': True, diff --git a/stock_reserve/data/stock_data.xml b/stock_reserve/data/stock_data.xml new file mode 100644 index 000000000..27832218d --- /dev/null +++ b/stock_reserve/data/stock_data.xml @@ -0,0 +1,9 @@ + + + + + Reservation Stock + + + + diff --git a/stock_reserve/model/stock_reserve.py b/stock_reserve/model/stock_reserve.py index b045a5ff0..c84b8c5f1 100644 --- a/stock_reserve/model/stock_reserve.py +++ b/stock_reserve/model/stock_reserve.py @@ -19,18 +19,12 @@ # ############################################################################## -from datetime import datetime from openerp.osv import orm, fields -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FMT class stock_reservation(orm.Model): """ Allow to reserve products. - - A stock reservation is basically a stock move, - but the reservation is handled by this model using - a ``_inherits``. """ _name = 'stock.reservation' _description = 'Stock Reservation' @@ -38,39 +32,17 @@ class stock_reservation(orm.Model): _columns = { 'move_id': fields.many2one('stock.move', - 'Move', - readonly=True, + 'Reservation Move', required=True, + readonly=True, ondelete='cascade', select=1), 'date_validity': fields.datetime('Validity Date'), } - def _prepare_reserve(self, cr, uid, product_id, quantity, location_id, - date_validity=None, context=None): - product_obj = self.pool.get('product.product') - product = product_obj.browse(cr, uid, product_id, context=context) - location_dest_id = self._get_reservation_location( - cr, uid, product_id, context=context) - vals = {'name': 'Reservation', # sequence? - 'product_id': product_id, - 'product_qty': quantity, - 'product_uom': product.uom_id.id, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'price_unit': product.standard_price or 0.0, - } - if date_validity: - vals.update({'date_validity': date_validity, - 'date': date_validity, - 'date_expected': date_validity, - }) - else: - today = datetime.now().strftime(DT_FMT) - vals.update({'date': today, - 'date_expected': today, - }) - return vals + _defaults = { + 'type': 'internal', + } def _get_reservation_location(self, cr, uid, product_id, context=None): """ Returns the appropriate destination location to @@ -78,42 +50,30 @@ class stock_reservation(orm.Model): """ return 1 - def reserve(self, cr, uid, product_id, quantity, location_id, - date_validity=None, context=None): - """ Reserve a product. + 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. - - :param product_id: id of the product to reserve - :param quantity: quantity of products to reserve - :param location_id: source location for the reservation - :param date_validity: optional datetime until which the reservation - is valid - :returns: id of the ``stock.reservation`` created """ - vals = self._prepare_reserve(cr, uid, product_id, quantity, - location_id, - date_validity=date_validity, - context=context) - reservation_id = self.create(cr, uid, vals, context=context) - move_id = self.read(cr, uid, - reservation_id, - ['move_id'], - context=context, - load='_classic_write')['move_id'] move_obj = self.pool.get('stock.move') - move_obj.action_confirm(cr, uid, [move_id], context=context) - # TODO: if no quantity in the location, it will stay 'confirmed' - # after action_confirm(), how to handle that? - move_obj.action_assign(cr, uid, [move_id]) - return reservation_id + reservations = self.browse(cr, uid, ids, context=context) + move_ids = [reserv.move_id.id for reserv in reservations] + move_obj.action_confirm(cr, uid, move_ids, context=context) + move_obj.force_assign(cr, uid, move_ids, context=context) + move_obj.action_done(cr, uid, move_ids, context=context) + return True def release(self, cr, uid, ids, context=None): - """ Release a reservation """ return self.unlink(cr, uid, ids, context=context) + def create(self, cr, uid, vals, context=None): + # TODO + return super(stock_reservation, self).create(cr, uid, vals, + context=context) + def unlink(self, cr, uid, ids, context=None): + """ Release the reservation """ if isinstance(ids, (int, long)): ids = [ids] reservations = self.read(cr, uid, ids, ['move_id'], @@ -121,4 +81,39 @@ class stock_reservation(orm.Model): 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 super(stock_reservation, self).unlink(cr, uid, ids, context=context) + return super(stock_reservation, self).unlink(cr, uid, ids, + context=context) + + def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, + loc_dest_id=False, partner_id=False): + """ On change of product id, if finds UoM, UoS, + quantity and UoS quantity. + """ + move_obj = self.pool.get('stock.move') + if ids: + reserv = self.read(cr, uid, ids, ['move_id'], load='_classic_write') + move_ids = [rv['move_id'] for rv in reserv] + else: + move_ids = [] + return move_obj.onchange_product_id( + cr, uid, move_ids, prod_id=prod_id, loc_id=loc_id, + loc_dest_id=loc_dest_id, partner_id=partner_id) + + def onchange_move_type(self, cr, uid, ids, type, context=None): + """ On change of move type gives source and destination location. + """ + move_obj = self.pool.get('stock.move') + location_obj = self.pool.get('stock.location') + data_obj = self.pool.get('ir.model.data') + result = move_obj.onchange_move_type(cr, uid, ids, type, + context=context) + get_ref = data_obj.get_object_reference + try: + __, dest_location_id = get_ref(cr, uid, 'stock_reserve', + 'stock_location_reservation') + location_obj.check_access_rule(cr, uid, [dest_location_id], + 'read', context=context) + except (orm.except_orm, ValueError): + dest_location_id = False + result['value']['location_dest_id'] = dest_location_id + return result diff --git a/stock_reserve/view/stock_reserve.xml b/stock_reserve/view/stock_reserve.xml new file mode 100644 index 000000000..27aeda5b0 --- /dev/null +++ b/stock_reserve/view/stock_reserve.xml @@ -0,0 +1,118 @@ + + + + + stock.reservation.form + stock.reservation + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + stock.reservation.tree + stock.reservation + + + + + + + + + + + + {"reload_on_button": 1} + + + + @@ -22,37 +24,45 @@