openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #00915
lp:~camptocamp/sale-wkfl/add-sale_validity-sale-stock-prebook-jge into lp:sale-wkfl
Joël Grand-Guillaume @ camptocamp has proposed merging lp:~camptocamp/sale-wkfl/add-sale_validity-sale-stock-prebook-jge into lp:sale-wkfl.
Commit message:
[ADD] sale_validity : Adds a validity date on the sales quotation defining until when the quotation is valid
[ADD] sale_stock_prebook : Allow to reserve stock (virtual quantity) during quotation
Requested reviews:
Sale Core Editors (sale-core-editors)
For more details, see:
https://code.launchpad.net/~camptocamp/sale-wkfl/add-sale_validity-sale-stock-prebook-jge/+merge/193602
Hi,
This proposal add 2 modules :
- sale_validity : Add a validity date on the sales quotation defining until when the quotation is valid
- sale_stock_prebook : Allow to reserve stock (virtual quantity) during quotation
I made one for both as they make sense together and sale_validity is a very small one. They are useful when people make to ensure a given stock will not be used while sending a quote to an important customer.
Regards,
--
https://code.launchpad.net/~camptocamp/sale-wkfl/add-sale_validity-sale-stock-prebook-jge/+merge/193602
Your team Sale Core Editors is requested to review the proposed merge of lp:~camptocamp/sale-wkfl/add-sale_validity-sale-stock-prebook-jge into lp:sale-wkfl.
=== added directory 'sale_stock_prebook'
=== added file 'sale_stock_prebook/__init__.py'
--- sale_stock_prebook/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/__init__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,2 @@
+import model
+import wizard
=== added file 'sale_stock_prebook/__openerp__.py'
--- sale_stock_prebook/__openerp__.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/__openerp__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Jacques-Etienne Baudoux
+# 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": "Sales Quotation Pre-booking of stock",
+ "version": "7.0.0",
+ "depends": ["sale_validity","sale_stock"],
+ "author": "Camptocamp",
+ "website": "http://www.camptocamp.com",
+ "category": "Sales",
+ "description": "Allow to reserve stock (virtual quantity) during quotation",
+ 'data': [
+ "wizard/sale_stock_prebook.xml",
+ "view/sale_order.xml",
+ ],
+ 'installable': True,
+ 'active': False,
+}
=== added directory 'sale_stock_prebook/model'
=== added file 'sale_stock_prebook/model/__init__.py'
--- sale_stock_prebook/model/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/model/__init__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,2 @@
+import sale_order
+import stock_move
=== added file 'sale_stock_prebook/model/sale_order.py'
--- sale_stock_prebook/model/sale_order.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/model/sale_order.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+
+from osv import fields, osv
+from tools.translate import _
+from openerp import SUPERUSER_ID
+
+"""
+* Pre-book stock while sale order is not yet confirmed.
+ Create a stock move (without picking and procurement) to decrease virtual stock. That reservation gets updated with the sale order line.
+ If a reservation is existing at order confirmation, use it in the generated picking.
+"""
+
+class sale_order(osv.osv):
+ _inherit = "sale.order"
+ _columns = {
+ 'is_prebookable': fields.function(lambda self,*args,**kwargs:self._get_is_prebooked(*args,**kwargs), multi='prebooked',
+ type='boolean', readonly=True,
+ string='Stock Pre-bookable', help="Are there products pre-bookable?",
+ ),
+ 'is_prebooked': fields.function(lambda self,*args,**kwargs:self._get_is_prebooked(*args,**kwargs), multi='prebooked',
+ type='boolean', readonly=True,
+ string='Stock Pre-booked', help="Are all products pre-booked in stock?",
+ invisible=True,
+ states={
+ 'draft':[('invisible',False)],
+ 'sent':[('invisible',False)],
+ },
+ ),
+ }
+
+ def _get_is_prebooked(self, cr, uid, ids, fields, args, context=None):
+ """ Is pre-booked if foreach order lines it's mto or there is a stock move"""
+ res={}
+ for order_id in ids:
+ res[order_id]={'is_prebookable':False,'is_prebooked':False}
+ line_ids=reduce(lambda x,y:x+y,[x['order_line'] for x in self.read(cr,uid,ids,['order_line'],context=context)],[])
+ for line in self.pool.get('sale.order.line').read(cr,uid,line_ids,['type','move_ids','order_id'],context=context,load='_classic_write'):
+ if line['type']!='make_to_order':
+ if line['move_ids']:
+ res[line['order_id']]['is_prebooked']=True
+ else:
+ res[line['order_id']]['is_prebookable']=True
+ return res
+ def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
+ """ Delete prebookings """
+ unlink_ids=[]
+ for line in order_lines:
+ for move in line.move_ids:
+ #we don't expect this method to be called outside quotation confirmation
+ assert move._model._is_prebooked(move), _("Internal Error")
+ unlink_ids.append(move.id)
+ unlink_ids and self.pool.get('sale.order.line')._prebook_cancel(cr,uid,unlink_ids,context)
+ return super(sale_order,self)._create_pickings_and_procurements(cr, uid, order, order_lines, picking_id=picking_id, context=context)
+
+ def button_prebook(self, cr, uid, ids, context):
+ orders=self.read(cr,uid,ids,['is_prebookable'],context=context)
+ if not reduce(lambda x,y:x or y,[x['is_prebookable'] for x in orders],False):
+ raise osv.except_osv(_('Warning!'), _('All products are already pre-booked in stock.'))
+ return {
+ 'name' : _('Pre-book products from stock'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.stock.prebook',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'target': 'new',
+ }
+ def button_prebook_cancel(self, cr, uid, ids, context):
+ line_ids=reduce(lambda x,y:x+y,[x['order_line'] for x in self.read(cr,SUPERUSER_ID,ids,['order_line'],context=context)],[])
+ line_ids and self.pool.get('sale.order.line')._prebook_cancel(cr,uid,line_ids,context=context)
+
+class sale_order_line(osv.osv):
+ _inherit = "sale.order.line"
+ _columns = {
+ 'date_prebooked': fields.function(lambda self,*args,**kwargs:self._get_date_prebooked(*args,**kwargs), multi='prebooked',
+ type='date', readonly=True,
+ string='Pre-booked Stock Until',
+ invisible=True,
+ states={
+ 'draft':[('invisible',False)],
+ 'sent':[('invisible',False)],
+ },
+ ),
+ }
+
+ def _get_date_prebooked(self, cr, uid, ids, fields, args, context=None):
+ res={}
+ for line in self.browse(cr,uid,ids,context=context):
+ res[line.id]={'date_prebooked':False}
+ if line.move_ids:
+ for move in line.move_ids:
+ if move._model._is_prebooked(move):
+ res[line.id]['date_prebooked']=move.date_validity
+ break
+ return res
+
+ def _prebook(self, cr, uid, ids, date_prebook, context=None):
+ sale_obj=self.pool.get('sale.order')
+ move_obj=self.pool.get('stock.move')
+ for line in self.browse(cr,SUPERUSER_ID,ids,context=context):
+ order=line.order_id
+ picking_id=False
+ date_planned = sale_obj._get_date_planned(cr, uid, order, line, order.date_order, context=context)
+ d_move=sale_obj._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=None)
+ #module sale_stock
+ # location_id = order.shop_id.warehouse_id.lot_stock_id.id
+ # output_id = order.shop_id.warehouse_id.lot_output_id.id
+ # return {
+ # 'name': line.name,
+ # 'picking_id': picking_id,
+ # 'product_id': line.product_id.id,
+ # 'date': date_planned,
+ # 'date_expected': date_planned,
+ # 'product_qty': line.product_uom_qty,
+ # 'product_uom': line.product_uom.id,
+ # 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
+ # 'product_uos': (line.product_uos and line.product_uos.id)\
+ # or line.product_uom.id,
+ # 'product_packaging': line.product_packaging.id,
+ # 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
+ # 'location_id': location_id,
+ # 'location_dest_id': output_id,
+ # 'sale_line_id': line.id,
+ # 'tracking_id': False,
+ # 'state': 'draft',
+ # #'state': 'waiting',
+ # 'company_id': order.company_id.id,
+ # 'price_unit': line.product_id.standard_price or 0.0
+ # }
+ d_move['state']='waiting' #ensure move don't get processed
+ d_move['date_validity']=date_prebook
+ if line.move_ids: #there are already moves linked to the line, don't create new one
+ for move in line.move_ids:
+ assert move._model._is_prebooked(move), _("Internal Error")
+ move_ids=[x['id'] for x in line.move_ids]
+ move_obj.write(cr, uid, move_ids, d_move, context=context)
+ subject="Updated pre-booking for %s"%line.name
+ else:
+ move_obj.create(cr, uid, d_move, context=context)
+ subject="Pre-booking for %s"%line.name
+ message="Stock has been pre-booked until %s"%date_prebook
+ sale_obj.message_post(cr, uid, [order.id], subject=subject, body=message, context=context)
+ def _prebook_cancel(self, cr, uid, ids, context=None):
+ if context is None:
+ context={}
+ sale_obj=self.pool.get('sale.order')
+ move_ids=[]
+ for line in self.browse(cr,SUPERUSER_ID,ids,context=context):
+ move_ids+=[x['id'] for x in line.move_ids if x._model._is_prebooked(x)]
+ # There is somethimes some trouble here, I comment it for demo
+ # subject="Cancelled pre-booking for %s"%line.name
+ # message="Stock pre-booking has been cancelled"
+ # sale_obj.message_post(cr, uid, [line.order_id.id], subject=subject, body=message, context=context)
+ if move_ids:
+ ctx=context.copy()
+ ctx['call_unlink']=True #allow to delete non draft moves
+ self.pool.get('stock.move').unlink(cr, uid, move_ids, context=ctx)
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Update pre-booking when line is changed"""
+ res = super(sale_order_line,self).write(cr,uid,ids,vals,context=context)
+ if vals.get('type','')=='make_to_order': #remove pre-booking if procurement method changed to mto
+ self._prebook_cancel(cr,uid,ids,context=context)
+ else: #update pre-booking if fields changed
+ fields=['product_id','product_qty','product_uom','product_uos_qty','product_uos','product_packaging','partner_id','price_unit']
+ if not set(vals.keys()).intersection(set(fields)):
+ return res
+ for line in self.read(cr,uid,ids,['date_prebooked'],context=context):
+ if line['date_prebooked']:
+ self._prebook(cr,uid,[line['id']],line['date_prebooked'],context=context)
+ return res
+
+ #TODO: remove pre-booking if cancelled
+ #def ...
+
+ def button_prebook_update(self, cr, uid, ids, context):
+ if context is None:
+ context={}
+ line=self.read(cr,uid,ids[0],['date_prebooked'],context=context)
+ ctx=context.copy()
+ ctx['default_date']=line['date_prebooked']
+ return {
+ 'name' : _('Pre-book products from stock'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.stock.prebook',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': ctx,
+ }
+ def button_prebook_cancel(self, cr, uid, ids, context):
+ self._prebook_cancel(cr,uid,ids,context=context)
+
=== added file 'sale_stock_prebook/model/stock_move.py'
--- sale_stock_prebook/model/stock_move.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/model/stock_move.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+from osv import fields, osv
+from tools.translate import _
+from openerp import SUPERUSER_ID
+
+"""
+* Add expiry date on pre-booked stock move
+"""
+
+#TODO: cron job to cancell pre-bookings based on validity date
+
+class stock_move(osv.osv):
+ _inherit = "stock.move"
+
+ _columns = {
+ 'date_validity': fields.date('Validity Date'),
+ }
+ def init(self, cr):
+ """ Index date_validity if filled """
+ indexname='stock_move_date_validity_index'
+ cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', (indexname,))
+ if not cr.fetchone():
+ cr.execute('CREATE INDEX %s ON %s (date_validity) WHERE date_validity IS NOT NULL'%(indexname,self._table))
+ def _is_prebooked(self,move):
+ return move.date_validity and move.state == 'waiting' and move.picking_id.id is False
+
=== added directory 'sale_stock_prebook/view'
=== added file 'sale_stock_prebook/view/sale_order.xml'
--- sale_stock_prebook/view/sale_order.xml 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/view/sale_order.xml 2013-11-01 13:38:00 +0000
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<openerp>
+<data>
+
+ <record id="view_order_form_prebook" model="ir.ui.view">
+ <field name="name">sale.order.form.prebook</field>
+ <field name="model">sale.order</field>
+ <field name="type">form</field>
+ <field name="inherit_id" ref="sale_validity.view_order_form_validity"/>
+ <field name="arch" type="xml">
+ <button name="action_quotation_send" position="before">
+ <button name="button_prebook" type="object"
+ states="draft,sent"
+ string="Pre-book stock" help="Pre-book products from stock"
+ context="{'default_date':date_validity}"
+ />
+ </button>
+
+ <xpath expr="//field[@name='order_line']//field[@name='state']" position="before">
+ <button name="%(action_sale_stock_prebook)d" type="action" states="draft" string="Pre-book stock" context="{'default_date':parent.date_validity}" attrs="{'invisible':[('date_prebooked','!=',False)]}" />
+ </xpath>
+ <xpath expr="//field[@name='order_line']//field[@name='type']" position="after">
+ <label for="date_prebooked" attrs="{'invisible':[('type','=','make_to_order')]}"/>
+ <div attrs="{'invisible':[('type','=','make_to_order')]}">
+ <field name="date_prebooked" class="oe_inline"/>
+ <button name="button_prebook_update" string="update" type="object" class="oe_link" attrs="{'invisible':[('date_prebooked','=',False)]}"/>
+ -
+ <button name="button_prebook_cancel" string="cancel" type="object" class="oe_link" attrs="{'invisible':[('date_prebooked','=',False)]}"/>
+ </div>
+ </xpath>
+
+ <field name="invoiced" position="before">
+ <label for="is_prebooked"/><div>
+ <field name="is_prebooked"/>
+ <button name="button_prebook_cancel" string="cancel all" type="object" class="oe_link" attrs="{'invisible':[('is_prebooked','=',False)]}"/>
+ </div>
+ </field>
+ </field>
+ </record>
+
+</data>
+</openerp>
=== added directory 'sale_stock_prebook/wizard'
=== added file 'sale_stock_prebook/wizard/__init__.py'
--- sale_stock_prebook/wizard/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/wizard/__init__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,1 @@
+import sale_stock_prebook
=== added file 'sale_stock_prebook/wizard/sale_stock_prebook.py'
--- sale_stock_prebook/wizard/sale_stock_prebook.py 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/wizard/sale_stock_prebook.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+from osv import osv, fields
+from tools.translate import _
+from openerp import SUPERUSER_ID
+
+class sale_stock_prebook(osv.osv_memory):
+ _name = 'sale.stock.prebook'
+ _columns = {
+ 'date': fields.date("Pre-book stock until", help="Define date until when stock will be pre-booked", required=True),
+ }
+ def button_confirm(self, cr, uid, ids, context):
+ wiz=self.browse(cr,uid,ids[0],context=context)
+ if context['active_model']=='sale.order':
+ order_line_ids=map(sum,[x['order_line'] for x in self.pool.get('sale.order').read(cr,uid,context['active_ids'],['order_line'],context=context,load='_classic_write')])
+ self.pool.get('sale.order.line')._prebook(cr,uid,order_line_ids,wiz.date,context=context)
+ elif context['active_model']=='sale.order.line':
+ self.pool.get('sale.order.line')._prebook(cr,uid,context['active_ids'],wiz.date,context=context)
+ return {'type': 'ir.actions.act_window_close'}
=== added file 'sale_stock_prebook/wizard/sale_stock_prebook.xml'
--- sale_stock_prebook/wizard/sale_stock_prebook.xml 1970-01-01 00:00:00 +0000
+++ sale_stock_prebook/wizard/sale_stock_prebook.xml 2013-11-01 13:38:00 +0000
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+<data>
+ <record id="view_sale_stock_prebook_form" model="ir.ui.view">
+ <field name="name">sale.stock.prebook.form</field>
+ <field name="model">sale.stock.prebook</field>
+ <field name="arch" type="xml">
+ <form string="Pre-book stock" version="7.0">
+ <group colspan="4">
+ <field name="date"/>
+ </group>
+ <footer>
+ <button icon="gtk-execute" string="Confirm" name="button_confirm" type="object" class="oe_highlight" />
+ or
+ <button icon="gtk-cancel" special="cancel" string="Cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ <record id="action_sale_stock_prebook" model="ir.actions.act_window">
+ <field name="name">Pre-book products from stock</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">sale.stock.prebook</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
+</data>
+</openerp>
=== added directory 'sale_validity'
=== added file 'sale_validity/__init__.py'
--- sale_validity/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_validity/__init__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,1 @@
+import model
=== added file 'sale_validity/__openerp__.py'
--- sale_validity/__openerp__.py 1970-01-01 00:00:00 +0000
+++ sale_validity/__openerp__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Jacques-Etienne Baudoux
+# 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": "Sales Quotation Validity Date",
+ "version": "7.0.0",
+ "depends": ["sale"],
+ "author": "Camptocamp",
+ "category": "Sales",
+ "website": "http://www.camptocamp.com",
+ "description": "Add a validity date on the sales quotation defining until when the quotation is valid",
+ 'data': [
+ "view/sale_order.xml",
+ ],
+ 'installable': True,
+ 'active': False,
+}
=== added directory 'sale_validity/model'
=== added file 'sale_validity/model/__init__.py'
--- sale_validity/model/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_validity/model/__init__.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,1 @@
+import sale_order
=== added file 'sale_validity/model/sale_order.py'
--- sale_validity/model/sale_order.py 1970-01-01 00:00:00 +0000
+++ sale_validity/model/sale_order.py 2013-11-01 13:38:00 +0000
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+from osv import fields, osv
+from tools.translate import _
+from openerp import SUPERUSER_ID
+
+"""
+* Pre-book stock while sale order is not yet confirmed.
+ Create a stock move (without picking and procurement) to decrease virtual stock. That reservation gets updated with the sale order line.
+ If a reservation is existing at order confirmation, use it in the generated picking.
+"""
+
+class sale_order(osv.osv):
+ _inherit = "sale.order"
+
+ _columns = {
+ 'date_validity': fields.date("Valid Until", help="Define date until when quotation is valid",
+ readonly=True,
+ states={
+ 'draft':[('readonly',False)],
+ 'sent':[('readonly',True)], #don't allow to modify validity date when quotation has been send
+ },
+ track_visibility='onchange',
+ ),
+ }
+
=== added directory 'sale_validity/view'
=== added file 'sale_validity/view/sale_order.xml'
--- sale_validity/view/sale_order.xml 1970-01-01 00:00:00 +0000
+++ sale_validity/view/sale_order.xml 2013-11-01 13:38:00 +0000
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<openerp>
+<data>
+
+ <record id="view_order_form_validity" model="ir.ui.view">
+ <field name="name">sale.order.form.validity</field>
+ <field name="model">sale.order</field>
+ <field name="type">form</field>
+ <field name="inherit_id" ref="sale.view_order_form"/>
+ <field name="arch" type="xml">
+ <field name="date_order" position="after">
+ <field name="date_validity"/>
+ </field>
+ </field>
+ </record>
+
+</data>
+</openerp>
Follow ups