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
+
+
+
+
+
+
+
+
+
+
diff --git a/stock_reserve/view/stock_reserve.xml b/stock_reserve/view/stock_reserve.xml
new file mode 100644
index 000000000..a15d08c8c
--- /dev/null
+++ b/stock_reserve/view/stock_reserve.xml
@@ -0,0 +1,140 @@
+
+
+
+
+ stock.reservation.form
+ stock.reservation
+
+
+
+
+
+
+ stock.reservation.tree
+ stock.reservation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stock.reservation.search
+ stock.reservation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Stock Reservations
+ stock.reservation
+ ir.actions.act_window
+ form
+
+
+ {'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.
+
+
+
+
+
+
+
diff --git a/stock_reserve_sale/__init__.py b/stock_reserve_sale/__init__.py
new file mode 100644
index 000000000..f2bf938cb
--- /dev/null
+++ b/stock_reserve_sale/__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 model
+from . import wizard
diff --git a/stock_reserve_sale/__openerp__.py b/stock_reserve_sale/__openerp__.py
new file mode 100644
index 000000000..99522e98c
--- /dev/null
+++ b/stock_reserve_sale/__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 Reserve Sales',
+ 'version': '0.1',
+ 'author': 'Camptocamp',
+ 'category': 'Warehouse',
+ 'license': 'AGPL-3',
+ 'complexity': 'normal',
+ 'images': [],
+ 'website': "http://www.camptocamp.com",
+ 'description': """
+Stock Reserve Sales
+===================
+
+Allows to create stock reservations for quotation lines before the
+confirmation of the quotation. The reservations might have a validity
+date and in any case they are lifted when the quotation is canceled or
+confirmed.
+
+Reservations can be done only on "make to stock" and stockable products.
+
+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.
+
+If you want to prevent sales orders to be confirmed when the stock is
+insufficient at the order date, you may want to install the
+`sale_exception_nostock` module.
+
+""",
+ 'depends': ['sale_stock',
+ 'stock_reserve',
+ ],
+ 'demo': [],
+ 'data': ['wizard/sale_stock_reserve_view.xml',
+ 'view/sale.xml',
+ 'view/stock_reserve.xml',
+ ],
+ 'auto_install': False,
+ 'test': ['test/sale_reserve.yml',
+ 'test/sale_line_reserve.yml',
+ ],
+ 'installable': True,
+ }
diff --git a/stock_reserve_sale/model/__init__.py b/stock_reserve_sale/model/__init__.py
new file mode 100644
index 000000000..5c9fc5067
--- /dev/null
+++ b/stock_reserve_sale/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 sale
+from . import stock_reserve
diff --git a/stock_reserve_sale/model/sale.py b/stock_reserve_sale/model/sale.py
new file mode 100644
index 000000000..71c3374ec
--- /dev/null
+++ b/stock_reserve_sale/model/sale.py
@@ -0,0 +1,184 @@
+# -*- 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 sale_order(orm.Model):
+ _inherit = 'sale.order'
+
+ def _stock_reservation(self, cr, uid, ids, fields, args, context=None):
+ result = {}
+ for order_id in ids:
+ result[order_id] = {'has_stock_reservation': False,
+ 'is_stock_reservable': False}
+ for sale in self.browse(cr, uid, ids, context=context):
+ for line in sale.order_line:
+ if line.reservation_ids:
+ result[sale.id]['has_stock_reservation'] = True
+ if line.is_stock_reservable:
+ result[sale.id]['is_stock_reservable'] = True
+ if sale.state not in ('draft', 'sent'):
+ result[sale.id]['is_stock_reservable'] = False
+ return result
+
+ _columns = {
+ 'has_stock_reservation': fields.function(
+ _stock_reservation,
+ type='boolean',
+ readonly=True,
+ multi='stock_reservation',
+ string='Has Stock Reservations'),
+ 'is_stock_reservable': fields.function(
+ _stock_reservation,
+ type='boolean',
+ readonly=True,
+ multi='stock_reservation',
+ string='Can Have Stock Reservations'),
+ }
+
+ def release_all_stock_reservation(self, cr, uid, ids, context=None):
+ sales = self.browse(cr, uid, ids, context=context)
+ line_ids = [line.id for sale in sales for line in sale.order_line]
+ line_obj = self.pool.get('sale.order.line')
+ line_obj.release_stock_reservation(cr, uid, line_ids, context=context)
+ return True
+
+ def action_button_confirm(self, cr, uid, ids, context=None):
+ self.release_all_stock_reservation(cr, uid, ids, context=context)
+ return super(sale_order, self).action_button_confirm(
+ cr, uid, ids, context=context)
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ self.release_all_stock_reservation(cr, uid, ids, context=context)
+ return super(sale_order, self).action_cancel(
+ cr, uid, ids, context=context)
+
+
+class sale_order_line(orm.Model):
+ _inherit = 'sale.order.line'
+
+ def _is_stock_reservable(self, cr, uid, ids, fields, args, context=None):
+ result = {}.fromkeys(ids, False)
+ for line in self.browse(cr, uid, ids, context=context):
+ if line.state != 'draft':
+ continue
+ if line.type == 'make_to_order':
+ continue
+ if (not line.product_id or line.product_id.type == 'service'):
+ continue
+ if not line.reservation_ids:
+ result[line.id] = True
+ return result
+
+ _columns = {
+ 'reservation_ids': fields.one2many(
+ 'stock.reservation',
+ 'sale_line_id',
+ string='Stock Reservation'),
+ 'is_stock_reservable': fields.function(
+ _is_stock_reservable,
+ type='boolean',
+ readonly=True,
+ string='Can be reserved'),
+ }
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ default['reservation_ids'] = False
+ return super(sale_order_line, self).copy_data(
+ cr, uid, id, default=default, context=context)
+
+ def release_stock_reservation(self, cr, uid, ids, context=None):
+ lines = self.browse(cr, uid, ids, context=context)
+ reserv_ids = [reserv.id for line in lines
+ for reserv in line.reservation_ids]
+ reserv_obj = self.pool.get('stock.reservation')
+ reserv_obj.release(cr, uid, reserv_ids, context=context)
+ return True
+
+ def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
+ uom=False, qty_uos=0, uos=False, name='', partner_id=False,
+ lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
+ result = super(sale_order_line, self).product_id_change(
+ cr, uid, ids, pricelist, product, qty=qty, uom=uom,
+ qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
+ lang=lang, update_tax=update_tax, date_order=date_order,
+ packaging=packaging, fiscal_position=fiscal_position,
+ flag=flag, context=context)
+ if not ids: # warn only if we change an existing line
+ return result
+ assert len(ids) == 1, "Expected 1 ID, got %r" % ids
+ line = self.browse(cr, uid, ids[0], context=context)
+ if qty != line.product_uom_qty and line.reservation_ids:
+ msg = _("As you changed the quantity of the line, "
+ "the quantity of the stock reservation will "
+ "be automatically adjusted to %.2f.") % qty
+ msg += "\n\n"
+ result.setdefault('warning', {})
+ if result['warning'].get('message'):
+ result['warning']['message'] += msg
+ else:
+ result['warning'] = {
+ 'title': _('Configuration Error!'),
+ 'message': msg,
+ }
+ return result
+
+ def write(self, cr, uid, ids, vals, context=None):
+ block_on_reserve = ('product_id', 'product_uom', 'product_uos',
+ 'type')
+ update_on_reserve = ('price_unit', 'product_uom_qty', 'product_uos_qty')
+ keys = set(vals.keys())
+ test_block = keys.intersection(block_on_reserve)
+ test_update = keys.intersection(update_on_reserve)
+ if test_block:
+ for line in self.browse(cr, uid, ids, context=context):
+ if not line.reservation_ids:
+ continue
+ raise orm.except_orm(
+ _('Error'),
+ _('You cannot change the product or unit of measure '
+ 'of lines with a stock reservation. '
+ 'Release the reservation '
+ 'before changing the product.'))
+ res = super(sale_order_line, self).write(cr, uid, ids, vals, context=context)
+ if test_update:
+ for line in self.browse(cr, uid, ids, context=context):
+ if not line.reservation_ids:
+ continue
+ if len(line.reservation_ids) > 1:
+ raise orm.except_orm(
+ _('Error'),
+ _('Several stock reservations are linked with the '
+ 'line. Impossible to adjust their quantity. '
+ 'Please release the reservation '
+ 'before changing the quantity.'))
+
+ line.reservation_ids[0].write(
+ {'price_unit': line.price_unit,
+ 'product_qty': line.product_uom_qty,
+ 'product_uos_qty': line.product_uos_qty,
+ }
+ )
+ return res
diff --git a/stock_reserve_sale/model/stock_reserve.py b/stock_reserve_sale/model/stock_reserve.py
new file mode 100644
index 000000000..0ea5fb958
--- /dev/null
+++ b/stock_reserve_sale/model/stock_reserve.py
@@ -0,0 +1,50 @@
+# -*- 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 stock_reservation(orm.Model):
+ _inherit = 'stock.reservation'
+
+ _columns = {
+ 'sale_line_id': fields.many2one(
+ 'sale.order.line',
+ string='Sale Order Line',
+ ondelete='cascade'),
+ 'sale_id': fields.related(
+ 'sale_line_id', 'order_id',
+ type='many2one',
+ relation='sale.order',
+ string='Sale Order')
+ }
+
+ def release(self, cr, uid, ids, context=None):
+ self.write(cr, uid, ids, {'sale_line_id': False}, context=context)
+ return super(stock_reservation, self).release(
+ cr, uid, ids, context=context)
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ default['sale_line_id'] = False
+ return super(stock_reservation, self).copy_data(
+ cr, uid, id, default=default, context=context)
diff --git a/stock_reserve_sale/test/sale_line_reserve.yml b/stock_reserve_sale/test/sale_line_reserve.yml
new file mode 100644
index 000000000..2dfbee6ef
--- /dev/null
+++ b/stock_reserve_sale/test/sale_line_reserve.yml
@@ -0,0 +1,118 @@
+-
+ I create a product to test the stock reservation
+-
+ !record {model: product.product, id: product_yogurt}:
+ default_code: 001yogurt
+ name: yogurt
+ 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 yogurt with 10 kgm
+-
+ !record {model: stock.change.product.qty, id: change_qty}:
+ new_quantity: 10
+ product_id: product_yogurt
+-
+ !python {model: stock.change.product.qty}: |
+ context['active_id'] = ref('product_yogurt')
+ self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
+-
+ In order to test reservation of the sales order, I create a sales order
+-
+ !record {model: sale.order, id: sale_reserve_02}:
+ partner_id: base.res_partner_2
+ payment_term: account.account_payment_term
+-
+ And I create a sales order line
+-
+ !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
+ name: Yogurt
+ product_id: product_yogurt
+ product_uom_qty: 4
+ product_uom: product.product_uom_kgm
+ order_id: sale_reserve_02
+-
+ And I create a stock reserve for this line
+-
+ !record {model: sale.stock.reserve, id: wizard_reserve_02_01}:
+ note: Reservation for the sales order line
+-
+ I call the wizard to reserve the products of the sales order
+-
+ !python {model: sale.stock.reserve}: |
+ active_id = ref('sale_line_reserve_02_01')
+ context['active_id'] = active_id
+ context['active_ids'] = [active_id]
+ context['active_model'] = 'sale.order.line'
+ self.button_reserve(cr, uid, [ref('wizard_reserve_02_01')], context=context)
+-
+ I check Virtual stock of yogurt after update reservation
+-
+ !python {model: product.product}: |
+ product = self.browse(cr, uid, ref('product_yogurt'), context=context)
+ assert product.virtual_available == 6, "Stock is not updated."
+-
+ And I create a MTO sales order line
+-
+ !record {model: sale.order.line, id: sale_line_reserve_02_02, view: sale.view_order_line_tree}:
+ order_id: sale_reserve_02
+ name: Mouse, Wireless
+ product_id: product.product_product_12
+ type: make_to_order
+ product_uom_qty: 4
+ product_uom: product.product_uom_kgm
+-
+ And I try to create a stock reserve for this MTO line
+-
+ !record {model: sale.stock.reserve, id: wizard_reserve_02_02}:
+ note: Reservation for the sales order line
+-
+ I call the wizard to reserve the products of the sales order
+-
+ !python {model: sale.stock.reserve}: |
+ active_id = ref('sale_line_reserve_02_02')
+ context['active_id'] = active_id
+ context['active_ids'] = [active_id]
+ context['active_model'] = 'sale.order.line'
+ self.button_reserve(cr, uid, [ref('wizard_reserve_02_02')], context=context)
+-
+ I should not have a stock reservation for a MTO line
+-
+ !python {model: stock.reservation}: |
+ reserv_ids = self.search(
+ cr, uid,
+ [('sale_line_id', '=', ref('sale_line_reserve_02_02'))],
+ context=context)
+ assert not reserv_ids, "No stock reservation should be created for MTO lines"
+-
+ And I change the quantity in the first line
+-
+ !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
+ product_uom_qty: 5
+-
+
+ I check Virtual stock of yogurt after change of reservations
+-
+ !python {model: product.product}: |
+ product = self.browse(cr, uid, ref('product_yogurt'), context=context)
+ assert product.virtual_available == 5, "Stock is not updated."
+-
+ I release the sales order's reservations for the first line
+-
+ !python {model: sale.order.line}: |
+ self.release_stock_reservation(cr, uid, [ref('sale_line_reserve_02_01')], context=context)
+-
+ I check Virtual stock of yogurt after release of reservations
+-
+ !python {model: product.product}: |
+ product = self.browse(cr, uid, ref('product_yogurt'), context=context)
+ assert product.virtual_available == 10, "Stock is not updated."
diff --git a/stock_reserve_sale/test/sale_reserve.yml b/stock_reserve_sale/test/sale_reserve.yml
new file mode 100644
index 000000000..7fc9dff6d
--- /dev/null
+++ b/stock_reserve_sale/test/sale_reserve.yml
@@ -0,0 +1,65 @@
+-
+ I create a product to test the stock reservation
+-
+ !record {model: product.product, id: product_gelato}:
+ default_code: 001GELATO
+ name: Gelato
+ 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 Gelato with 10 kgm
+-
+ !record {model: stock.change.product.qty, id: change_qty}:
+ new_quantity: 10
+ product_id: product_gelato
+-
+ !python {model: stock.change.product.qty}: |
+ context['active_id'] = ref('product_gelato')
+ self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
+-
+ In order to test reservation of the sales order, I create a sales order
+-
+ !record {model: sale.order, id: sale_reserve_01}:
+ partner_id: base.res_partner_2
+ payment_term: account.account_payment_term
+ order_line:
+ - product_id: product_gelato
+ product_uom_qty: 4
+-
+ I call the wizard to reserve the products of the sales order
+-
+ !record {model: sale.stock.reserve, id: wizard_reserve_01}:
+ note: Reservation for the sales order
+-
+ !python {model: sale.stock.reserve}: |
+ active_id = ref('sale_reserve_01')
+ context['active_id'] = active_id
+ context['active_ids'] = [active_id]
+ context['active_model'] = 'sale.order'
+ self.button_reserve(cr, uid, [ref('wizard_reserve_01')], context=context)
+-
+ I check Virtual stock of Gelato after update reservation
+-
+ !python {model: product.product}: |
+ product = self.browse(cr, uid, ref('product_gelato'), context=context)
+ assert product.virtual_available == 6, "Stock is not updated."
+-
+ I release the sales order's reservations
+-
+ !python {model: sale.order}: |
+ self.release_all_stock_reservation(cr, uid, [ref('sale_reserve_01')], context=context)
+-
+ I check Virtual stock of Gelato after release of reservations
+-
+ !python {model: product.product}: |
+ product = self.browse(cr, uid, ref('product_gelato'), context=context)
+ assert product.virtual_available == 10, "Stock is not updated."
diff --git a/stock_reserve_sale/view/sale.xml b/stock_reserve_sale/view/sale.xml
new file mode 100644
index 000000000..e5849b04c
--- /dev/null
+++ b/stock_reserve_sale/view/sale.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+ sale.order.form.reserve
+ sale.order
+
+
+
+
+
+ {"reload_on_button": 1}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stock_reserve_sale/view/stock_reserve.xml b/stock_reserve_sale/view/stock_reserve.xml
new file mode 100644
index 000000000..71e47cb95
--- /dev/null
+++ b/stock_reserve_sale/view/stock_reserve.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ stock.reservation.form
+ stock.reservation
+
+
+
+
+
+
+
+
+
+
+
+
+ stock.reservation.tree
+ stock.reservation
+
+
+
+
+
+
+
+
+
+
diff --git a/stock_reserve_sale/wizard/__init__.py b/stock_reserve_sale/wizard/__init__.py
new file mode 100644
index 000000000..6156962e3
--- /dev/null
+++ b/stock_reserve_sale/wizard/__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 sale_stock_reserve
diff --git a/stock_reserve_sale/wizard/sale_stock_reserve.py b/stock_reserve_sale/wizard/sale_stock_reserve.py
new file mode 100644
index 000000000..5499c7e41
--- /dev/null
+++ b/stock_reserve_sale/wizard/sale_stock_reserve.py
@@ -0,0 +1,111 @@
+# -*- 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 sale_stock_reserve(orm.TransientModel):
+ _name = 'sale.stock.reserve'
+
+ _columns = {
+ 'location_id': fields.many2one(
+ 'stock.location',
+ 'Source Location',
+ required=True),
+ 'location_dest_id': fields.many2one(
+ 'stock.location',
+ 'Reservation Location',
+ required=True,
+ help="Location where the system will reserve the "
+ "products."),
+ 'date_validity': fields.date(
+ "Validity Date",
+ help="If a date is given, the reservations will be released "
+ "at the end of the validity."),
+ 'note': fields.text('Notes'),
+ }
+
+ def _default_location_id(self, cr, uid, context=None):
+ reserv_obj = self.pool.get('stock.reservation')
+ return reserv_obj._default_location_id(cr, uid, context=context)
+
+ def _default_location_dest_id(self, cr, uid, context=None):
+ reserv_obj = self.pool.get('stock.reservation')
+ return reserv_obj._default_location_dest_id(cr, uid, context=context)
+
+ _defaults = {
+ 'location_id': _default_location_id,
+ 'location_dest_id': _default_location_dest_id,
+ }
+
+ def _prepare_stock_reservation(self, cr, uid, form, line, context=None):
+ product_uos = line.product_uos.id if line.product_uos else False
+ return {'product_id': line.product_id.id,
+ 'product_uom': line.product_uom.id,
+ 'product_qty': line.product_uom_qty,
+ 'date_validity': form.date_validity,
+ 'name': "{} ({})".format(line.order_id.name, line.name),
+ 'location_id': form.location_id.id,
+ 'location_dest_id': form.location_dest_id.id,
+ 'note': form.note,
+ 'product_uos_qty': line.product_uos_qty,
+ 'product_uos': product_uos,
+ 'price_unit': line.price_unit,
+ 'sale_line_id': line.id,
+ }
+
+ def stock_reserve(self, cr, uid, ids, line_ids, context=None):
+ assert len(ids) == 1, "Expected 1 ID, got %r" % ids
+ reserv_obj = self.pool.get('stock.reservation')
+ line_obj = self.pool.get('sale.order.line')
+
+ form = self.browse(cr, uid, ids[0], context=context)
+ lines = line_obj.browse(cr, uid, line_ids, context=context)
+ for line in lines:
+ if not line.is_stock_reservable:
+ continue
+ vals = self._prepare_stock_reservation(cr, uid, form, line,
+ context=context)
+ reserv_id = reserv_obj.create(cr, uid, vals, context=context)
+ reserv_obj.reserve(cr, uid, [reserv_id], context=context)
+ return True
+
+ def button_reserve(self, cr, uid, ids, context=None):
+ assert len(ids) == 1, "Expected 1 ID, got %r" % ids
+ if context is None:
+ context = {}
+ close = {'type': 'ir.actions.act_window_close'}
+ active_model = context.get('active_model')
+ active_ids = context.get('active_ids')
+ if not (active_model and active_ids):
+ return close
+
+ line_obj = self.pool.get('sale.order.line')
+ if active_model == 'sale.order':
+ sale_obj = self.pool.get('sale.order')
+ sales = sale_obj.browse(cr, uid, active_ids, context=context)
+ line_ids = [line.id for sale in sales for line in sale.order_line]
+
+ if active_model == 'sale.order.line':
+ line_ids = active_ids
+
+ self.stock_reserve(cr, uid, ids, line_ids, context=context)
+ return close
diff --git a/stock_reserve_sale/wizard/sale_stock_reserve_view.xml b/stock_reserve_sale/wizard/sale_stock_reserve_view.xml
new file mode 100644
index 000000000..5b3c39bcc
--- /dev/null
+++ b/stock_reserve_sale/wizard/sale_stock_reserve_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+ sale.stock.reserve.form
+ sale.stock.reserve
+
+
+
+
+
+
+ Reserve Stock for Quotation Lines
+ ir.actions.act_window
+ sale.stock.reserve
+ form
+ form
+ new
+
+
+