← 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-08 10:05:23 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    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
+from . 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-08 10:05:23 +0000
@@ -0,0 +1,33 @@
+# -*- 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-08 10:05:23 +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-08 10:05:23 +0000
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    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/>.
+#
+##############################################################################
+"""
+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.
+
+"""
+import itertools
+from osv import fields, osv
+from tools.translate import _
+from openerp import SUPERUSER_ID
+
+WATCHED_KEYS = ['product_id', 'product_qty', 'product_uom', 'product_uos_qty',
+                'product_uos', 'product_packaging', 'partner_id', 'price_unit']
+
+
+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 tells if order lines are mto or if there is a stock move"""
+        res = {}
+        for order_id in ids:
+            res[order_id] = {'is_prebookable': False, 'is_prebooked': False}
+        line_ids = [x['order_line'] for x in
+                        self.read(cr, uid, ids, ['order_line'], context=context)]
+        line_ids = list(itertools.chain.from_iterable(line_ids))
+
+        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 any((x['is_prebookable'] for x in orders)):
+            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 = [x['order_line'] for x in
+                        self.read(cr, SUPERUSER_ID, ids, ['order_line'], context=context)]
+        line_ids = list(itertools.chain.from_iterable(line_ids))
+        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)
+            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 = {}
+        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)]
+        if move_ids:
+            ctx = context.copy()
+            ctx['call_unlink'] = True  # Allows 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)
+        # Remove pre-booking if procurement method changed to mto
+        if vals.get('type', '') == 'make_to_order':
+            self._prebook_cancel(cr, uid, ids, context=context)
+        # Update pre-booking if fields changed
+        else:
+            fields = WATCHED_KEYS
+            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-08 10:05:23 +0000
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    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 osv import fields, osv
+"""Add expiry date on pre-booked stock move"""
+
+
+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-08 10:05:23 +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-08 10:05:23 +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-08 10:05:23 +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-08 10:05:23 +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-08 10:05:23 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    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

=== added file 'sale_validity/__openerp__.py'
--- sale_validity/__openerp__.py	1970-01-01 00:00:00 +0000
+++ sale_validity/__openerp__.py	2013-11-08 10:05:23 +0000
@@ -0,0 +1,39 @@
+# -*- 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": """
+Sale order validity date
+========================
+
+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-08 10:05:23 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    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 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-08 10:05:23 +0000
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    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 osv import fields, osv
+
+
+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)],
+                                             },
+                                             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-08 10:05:23 +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