reactivate module stock_reserve

This commit is contained in:
Yannick Vaucher
2014-09-01 15:24:57 +02:00
committed by aliciagarzo
parent f6404606ba
commit 424f7cd8f2
10 changed files with 646 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,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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{'name': 'Stock Reservation',
'summary': 'Stock reservations on products',
'version': '0.1',
'author': 'Camptocamp',
'category': 'Warehouse',
'license': 'AGPL-3',
'complexity': 'normal',
'images': [],
'website': "http://www.camptocamp.com",
'description': """
Stock Reservation
=================
Allows to create stock reservations on products.
Each reservation can have a validity date, once passed, the reservation
is automatically lifted.
The reserved products are substracted from the virtual stock. It means
that if you reserved a quantity of products which bring the virtual
stock below the minimum, the orderpoint will be triggered and new
purchase orders will be generated. It also implies that the max may be
exceeded if the reservations are canceled.
Contributors
------------
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
""",
'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,84 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2013 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
reservation_count = fields.Integer(
compute='_reservation_count',
string='# Sales')
@api.multi
def _reservation_count(self):
StockReservation = self.env['stock.reservation']
product_ids = self._get_products()
domain = [('product_id', 'in', product_ids),
('state', 'in', ['draft', 'assigned'])]
reservations = StockReservation.search(domain)
self.reservation_count = sum(reserv.product_uom_qty
for reserv in reservations)
@api.multi
def action_view_reservations(self):
assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids
ref = 'stock_reserve.action_stock_reservation_tree'
product_ids = self._get_products()
action_dict = self._get_act_window_dict(ref)
action_dict['domain'] = ("[('product_id','in',[" +
','.join(map(str, product_ids)) + "])]")
action_dict['context'] = {
'search_default_draft': 1,
'search_default_reserved': 1
}
return action_dict
class ProductProduct(models.Model):
_inherit = 'product.product'
reservation_count = fields.Integer(
compute='_reservation_count',
string='# Sales')
@api.multi
def _reservation_count(self):
StockReservation = self.env['stock.reservation']
product_id = self._ids[0]
domain = [('product_id', '=', product_id),
('state', 'in', ['draft', 'assigned'])]
reservations = StockReservation.search(domain)
self.reservation_count = sum(reserv.product_uom_qty
for reserv in reservations)
@api.multi
def action_view_reservations(self):
assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids
ref = 'stock_reserve.action_stock_reservation_tree'
product_id = self._ids[0]
action_dict = self.product_tmpl_id._get_act_window_dict(ref)
action_dict['domain'] = ("[('product_id','='," + str(product_id) + ")]")
action_dict['context'] = {
'search_default_draft': 1,
'search_default_reserved': 1
}
return action_dict

View File

@@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2013 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api
from openerp.exceptions import except_orm
from openerp.tools.translate import _
class StockReservation(models.Model):
""" Allow to reserve products.
The fields mandatory for the creation of a reservation are:
* product_id
* product_uom_qty
* product_uom
* name
The following fields are required but have default values that you may
want to override:
* company_id
* location_id
* dest_location_id
Optionally, you may be interested to define:
* date_validity (once passed, the reservation will be released)
* note
"""
_name = 'stock.reservation'
_description = 'Stock Reservation'
_inherits = {'stock.move': 'move_id'}
move_id = fields.Many2one(
'stock.move',
'Reservation Move',
required=True,
readonly=True,
ondelete='cascade',
select=1)
date_validity = fields.Date('Validity Date')
@api.model
def default_get(self, fields_list):
"""
Ensure default value of computed field `product_qty` is not set
as it would raise an error
"""
res = super(StockReservation, self).default_get(fields_list)
if 'product_qty' in res:
del res['product_qty']
return res
@api.model
def get_location_from_ref(self, ref):
""" Get a location from a xmlid if allowed
:param ref: tuple (module, xmlid)
"""
data_obj = self.env['ir.model.data']
try:
location = data_obj.xmlid_to_object(ref, raise_if_not_found=True)
location.check_access_rule('read')
location_id = location.id
except (except_orm, ValueError):
location_id = False
return location_id
@api.model
def _default_picking_type_id(self):
""" Search for an internal picking type
"""
move_obj = self.env['stock.move']
type_obj = self.env['stock.picking.type']
types = type_obj.search([('code', '=', 'internal')], limit=1)
if types:
return types[0].id
return False
@api.model
def _default_location_id(self):
move_obj = self.env['stock.move']
picking_type_id = self._default_picking_type_id()
return (move_obj
.with_context(default_picking_type_id=picking_type_id)
._default_location_source())
@api.model
def _default_location_dest_id(self):
ref = 'stock_reserve.stock_location_reservation'
return self.get_location_from_ref(ref)
_defaults = {
'picking_type_id': _default_picking_type_id,
'location_id': _default_location_id,
'location_dest_id': _default_location_dest_id,
'product_uom_qty': 1.0,
}
@api.multi
def reserve(self):
""" Confirm a reservation
The reservation is done using the default UOM of the product.
A date until which the product is reserved can be specified.
"""
move_recs = self.move_id
move_recs.date_expected = fields.Datetime.now()
move_recs.action_confirm()
move_recs.force_assign()
return True
@api.multi
def release(self):
"""
Releas moves from reservation
"""
move_recs = self.move_id
move_recs.action_cancel()
return True
@api.model
def release_validity_exceeded(self, ids=None):
""" Release all the reservation having an exceeded validity date """
domain = [('date_validity', '<', fields.date.today()),
('state', '=', 'assigned')]
if ids:
domain.append(('id', 'in', ids))
reserv_ids = self.search(domain)
self.release(reserv_ids)
return True
@api.multi
def unlink(self):
""" Release the reservation before the unlink """
self.release()
return super(StockReservation, self).unlink()
@api.onchange('product_id')
def _onchange_product_id(self):
""" set product_uom and name from product onchange """
# save value before reading of self.move_id as this last one erase
# product_id value
product = self.product_id
# WARNING this gettattr erase self.product_id
move = self.move_id
result = move.onchange_product_id(
prod_id=product.id, loc_id=False, loc_dest_id=False,
partner_id=False)
if result.get('value'):
vals = result['value']
# only keep the existing fields on the view
self.name = vals.get('name')
self.product_uom = vals.get('product_uom')
# repeat assignation of product_id so we don't loose it
self.product_id = product.id
@api.onchange('product_uom_qty')
def _onchange_quantity(self):
""" On change of product quantity avoid negative quantities """
if not self.product_id or self.product_uom_qty <= 0.0:
self.product_uom_qty = 0.0
@api.multi
def open_move(self):
assert len(self._ids) == 1, "1 ID expected, got %r" % self._ids
reserv = self[0].move_id
data_obj = self.env['ir.model.data']
ref_form2 = 'stock.action_move_form2'
action = data_obj.xmlid_to_object(ref_form2)
action_dict = action.read()
action_dict['name'] = _('Reservation Move')
# open directly in the form view
ref_form = 'stock.view_move_form'
view_id = data_obj.xmlid_to_res_id(ref_form)
action['views'] = [(view_id, 'form')]
action['res_id'] = reserv['move_id']
return action

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,58 @@
-
I create a product to test the stock reservation
-
!record {model: product.product, id: product_sorbet}:
default_code: 001SORBET
name: Sorbet
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 70.0
uom_id: product.product_uom_kgm
uom_po_id: product.product_uom_kgm
-
I update the current stock of the Sorbet with 10 kgm
-
!record {model: stock.change.product.qty, id: change_qty}:
new_quantity: 10
product_id: product_sorbet
-
!python {model: stock.change.product.qty}: |
context['active_id'] = ref('stock_reserve.product_sorbet')
self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
-
I check Virtual stock of Sorbet after update stock.
-
!python {model: product.product}: |
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
assert product.virtual_available == 10, "Stock is not updated."
-
I create a stock reservation for 5 kgm
-
!record {model: stock.reservation, id: reserv_sorbet1}:
product_id: product_sorbet
product_uom_qty: 5.0
product_uom: product.product_uom_kgm
name: reserve 5 kgm of sorbet for test
-
I confirm the reservation
-
!python {model: stock.reservation}: |
self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context)
-
I check Virtual stock of Sorbet after update reservation
-
!python {model: product.product}: |
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
assert product.virtual_available == 5, "Stock is not updated."
-
I release the reservation
-
!python {model: stock.reservation}: |
self.release(cr, uid, [ref('reserv_sorbet1')], context=context)
-
I check Virtual stock of Sorbet after update reservation
-
!python {model: product.product}: |
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
assert product.virtual_available == 10, "Stock is not updated."

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record model="ir.ui.view" id="product_template_form_view_reservation_button">
<field name="name">product.template.reservation.button</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button" name="action_view_reservations"
type="object" groups="base.group_sale_salesman" icon="fa-lock">
<field string="Stock Reservations" name="reservation_count" widget="statinfo" />
</button>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="product_product_form_view_reservation_button">
<field name="name">product.template.reservation.button</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button" name="action_view_reservations"
type="object" groups="base.group_sale_salesman" icon="fa-lock">
<field string="Stock Reservations" name="reservation_count" widget="statinfo" />
</button>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,135 @@
<?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" />
<label for="product_uom_qty" />
<div>
<field name="product_uom_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_uom_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_tree" 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_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_tree"
id="menu_action_stock_reservation"
parent="stock.menu_stock_inventory_control"
sequence="30"/>
</data>
</openerp>