← Back to team overview

openerp-community-reviewer team mailing list archive

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