[ADD] started a generic stock reservation module (stock_reserve), it will serve as a basis for the sale pre-book

This commit is contained in:
Guewen Baconnier
2013-09-05 14:06:46 +02:00
committed by aliciagarzo
parent a57441b2f9
commit d6fbbf0d96
10 changed files with 571 additions and 0 deletions

22
stock_reserve/__init__.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import model

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'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,
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="stock_location_reservation" model="stock.location">
<field name="name">Reservation Stock</field>
<field name="location_id" ref="stock.stock_location_company"/>
</record>
<!-- Release the stock.reservation when the validity date has
passed -->
<record forcecreate="True" id="ir_cron_release_stock_reservation" model="ir.cron">
<field name="name">Release the stock reservation having a passed validity date</field>
<field eval="True" name="active" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="model">stock.reservation</field>
<field name="function">release_validity_exceeded</field>
<field name="args">()</field>
</record>
</data>
</openerp>

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import stock_reserve
from . import product

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_reservation_manager stock.reservation manager model_stock_reservation stock.group_stock_manager 1 1 1 1
3 access_stock_reservation_user stock.reservation user model_stock_reservation stock.group_stock_user 1 1 1 0

View File

@@ -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."

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record model="ir.ui.view" id="product_form_view">
<field name="name">product.product.form.reserve</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="procurement.product_form_view_procurement_button"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button string="Stock Reservations"
name="open_stock_reservation"
type="object"/>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="view_stock_reservation_form" model="ir.ui.view">
<field name="name">stock.reservation.form</field>
<field name="model">stock.reservation</field>
<field name="arch" type="xml">
<form string="Stock Reservations" version="7.0">
<header>
<button name="reserve" type="object"
string="Reserve"
class="oe_highlight"
states="draft"/>
<button name="release" type="object"
string="Release"
class="oe_highlight"
states="assigned,confirmed,done"/>
<button name="open_move" type="object"
string="View Reservation Move"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,assigned"/>
</header>
<sheet>
<group>
<group name="main_grp" string="Details">
<field name="product_id"
on_change="onchange_product_id(product_id)"
/>
<label for="product_qty" />
<div>
<field name="product_qty"
on_change="onchange_quantity(product_id, product_qty)"
class="oe_inline"/>
<field name="product_uom"
groups="product.group_uom" class="oe_inline"/>
</div>
<field name="name"/>
<field name="date_validity" />
<field name="create_date" groups="base.group_no_one"/>
<field name="company_id"
groups="base.group_multi_company"
widget="selection"/>
</group>
<group name="location" string="Locations"
groups="stock.group_locations">
<field name="location_id"/>
<field name="location_dest_id"/>
</group>
<group name="note" string="Notes">
<field name="note" nolabel="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_stock_reservation_tree" model="ir.ui.view">
<field name="name">stock.reservation.tree</field>
<field name="model">stock.reservation</field>
<field name="arch" type="xml">
<tree string="Stock Reservations" version="7.0"
colors="blue:state == 'draft';grey:state == 'cancel'" >
<field name="name" />
<field name="product_id" />
<field name="move_id" />
<field name="product_qty" sum="Total" />
<field name="product_uom" />
<field name="date_validity" />
<field name="state"/>
<button name="reserve" type="object"
string="Reserve"
icon="terp-locked"
states="draft"/>
<button name="release" type="object"
string="Release"
icon="gtk-undo"
states="assigned,confirmed,done"/>
</tree>
</field>
</record>
<record id="view_stock_reservation_search" model="ir.ui.view">
<field name="name">stock.reservation.search</field>
<field name="model">stock.reservation</field>
<field name="arch" type="xml">
<search string="Stock Reservations" version="7.0">
<filter name="draft" string="Draft"
domain="[('state', '=', 'draft')]"
help="Not already reserved"/>
<filter name="reserved" string="Reserved"
domain="[('state', '=', 'assigned')]"
help="Moves are reserved."/>
<filter name="cancel" string="Released"
domain="[('state', '=', 'cancel')]"
help="Reservations have been released."/>
<field name="name" />
<field name="product_id" />
<field name="move_id" />
<group expand="0" string="Group By...">
<filter string="Status"
name="groupby_state"
domain="[]" context="{'group_by': 'state'}"/>
<filter string="Product" domain="[]"
name="groupby_product"
context="{'group_by': 'product_id'}"/>
<filter string="Product UoM" domain="[]"
name="groupby_product_uom"
context="{'group_by': 'product_uom'}"/>
</group>
</search>
</field>
</record>
<record id="action_stock_reservation" model="ir.actions.act_window">
<field name="name">Stock Reservations</field>
<field name="res_model">stock.reservation</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_stock_reservation_tree"/>
<field name="search_view_id" ref="view_stock_reservation_search"/>
<field name="context">{'search_default_draft': 1,
'search_default_reserved': 1,
'search_default_groupby_product': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a stock reservation.
</p><p>
This menu allow you to prepare and reserve some quantities
of products.
</p>
</field>
</record>
<menuitem action="action_stock_reservation"
id="menu_action_stock_reservation"
parent="stock.menu_stock_inventory_control"
sequence="30"/>
</data>
</openerp>