[MERGE] mail/chatter complete review/refactoring
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
25 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
30 import netsvc
31
32 class sale_shop(osv.osv):
33     _name = "sale.shop"
34     _description = "Sales Shop"
35     _columns = {
36         'name': fields.char('Shop Name', size=64, required=True),
37         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41         'company_id': fields.many2one('res.company', 'Company', required=False),
42     }
43     _defaults = {
44         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
45     }
46
47 sale_shop()
48
49 class sale_order(osv.osv):
50     _name = "sale.order"
51     _inherit = ['mail.thread', 'ir.needaction_mixin']
52     _description = "Sales Order"
53
54
55     def copy(self, cr, uid, id, default=None, context=None):
56         if not default:
57             default = {}
58         default.update({
59             'state': 'draft',
60             'shipped': False,
61             'invoice_ids': [],
62             'picking_ids': [],
63             'date_confirm': False,
64             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
65         })
66         return super(sale_order, self).copy(cr, uid, id, default, context=context)
67
68     def _amount_line_tax(self, cr, uid, line, context=None):
69         val = 0.0
70         for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.product_id, line.order_id.partner_id)['taxes']:
71             val += c.get('amount', 0.0)
72         return val
73
74     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75         cur_obj = self.pool.get('res.currency')
76         res = {}
77         for order in self.browse(cr, uid, ids, context=context):
78             res[order.id] = {
79                 'amount_untaxed': 0.0,
80                 'amount_tax': 0.0,
81                 'amount_total': 0.0,
82             }
83             val = val1 = 0.0
84             cur = order.pricelist_id.currency_id
85             for line in order.order_line:
86                 val1 += line.price_subtotal
87                 val += self._amount_line_tax(cr, uid, line, context=context)
88             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
91         return res
92
93     # This is False
94     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
95         if not ids:
96             return {}
97         res = {}
98         tmp = {}
99         for id in ids:
100             tmp[id] = {'picked': 0.0, 'total': 0.0}
101         cr.execute('''SELECT
102                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
103             FROM
104                 stock_move m
105             LEFT JOIN
106                 stock_picking p on (p.id=m.picking_id)
107             LEFT JOIN
108                 procurement_order mp on (mp.move_id=m.id)
109             WHERE
110                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
111
112         for item in cr.dictfetchall():
113             if item['move_state'] == 'cancel':
114                 continue
115
116             if item['picking_type'] == 'in':#this is a returned picking
117                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
120             else:
121                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
124
125         for order in self.browse(cr, uid, ids, context=context):
126             if order.shipped:
127                 res[order.id] = 100.0
128             else:
129                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130         return res
131
132     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
133         res = {}
134         for sale in self.browse(cursor, user, ids, context=context):
135             if sale.invoiced:
136                 res[sale.id] = 100.0
137                 continue
138             tot = 0.0
139             for invoice in sale.invoice_ids:
140                 if invoice.state not in ('draft', 'cancel'):
141                     tot += invoice.amount_untaxed
142             if tot:
143                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
144             else:
145                 res[sale.id] = 0.0
146         return res
147
148     def _invoiced(self, cursor, user, ids, name, arg, context=None):
149         res = {}
150         for sale in self.browse(cursor, user, ids, context=context):
151             res[sale.id] = True
152             invoice_existence = False
153             for invoice in sale.invoice_ids:
154                 if invoice.state!='cancel':
155                     invoice_existence = True
156                     if invoice.state != 'paid':
157                         res[sale.id] = False
158                         break
159             if not invoice_existence:
160                 res[sale.id] = False
161         return res
162
163     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
164         if not len(args):
165             return []
166         clause = ''
167         sale_clause = ''
168         no_invoiced = False
169         for arg in args:
170             if arg[1] == '=':
171                 if arg[2]:
172                     clause += 'AND inv.state = \'paid\''
173                 else:
174                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
175                     sale_clause = ',  sale_order AS sale '
176                     no_invoiced = True
177
178         cursor.execute('SELECT rel.order_id ' \
179                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180                 'WHERE rel.invoice_id = inv.id ' + clause)
181         res = cursor.fetchall()
182         if no_invoiced:
183             cursor.execute('SELECT sale.id ' \
184                     'FROM sale_order AS sale ' \
185                     'WHERE sale.id NOT IN ' \
186                         '(SELECT rel.order_id ' \
187                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188             res.extend(cursor.fetchall())
189         if not res:
190             return [('id', '=', 0)]
191         return [('id', 'in', [x[0] for x in res])]
192
193     def _get_order(self, cr, uid, ids, context=None):
194         result = {}
195         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196             result[line.order_id.id] = True
197         return result.keys()
198
199     _columns = {
200         'name': fields.char('Order Reference', size=64, required=True,
201             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
202         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
203         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204         'client_order_ref': fields.char('Customer Reference', size=64),
205         'state': fields.selection([
206             ('draft', 'Draft Quotation'),
207             ('sent', 'Quotation Sent'),
208             ('cancel', 'Cancelled'),
209             ('waiting_date', 'Waiting Schedule'),
210             ('progress', 'Sale Order'),
211             ('manual', 'Sale to Invoice'),
212             ('shipping_except', 'Shipping Exception'),
213             ('invoice_except', 'Invoice Exception'),
214             ('done', 'Done'),
215             ], 'Status', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
216         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
217         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
218         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
219         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
220         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
221         'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
222         'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."),
223
224         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
225         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
226             'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
227             help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
228         'order_policy': fields.selection([
229                 ('manual', 'On Demand'),
230                 ('picking', 'On Delivery Order'),
231                 ('prepaid', 'Before Delivery'),
232             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
233             help="""This field controls how invoice and delivery operations are synchronized.
234   - With 'On Demand', the invoice is created manually when needed.
235   - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
236   - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
237         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
238         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
239
240         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
241         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
242         'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
243         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
244         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246         'invoiced': fields.function(_invoiced, string='Paid',
247             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248         'note': fields.text('Terms and conditions'),
249
250         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
251             store = {
252                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254             },
255             multi='sums', help="The amount without tax."),
256         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
257             store = {
258                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
260             },
261             multi='sums', help="The tax amount."),
262         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
263             store = {
264                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
266             },
267             multi='sums', help="The total amount."),
268
269         'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
270         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
273     }
274     _defaults = {
275         'picking_policy': 'direct',
276         'date_order': fields.date.context_today,
277         'order_policy': 'manual',
278         'state': 'draft',
279         'user_id': lambda obj, cr, uid, context: uid,
280         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281         'invoice_quantity': 'order',
282         'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
283         'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
284     }
285     _sql_constraints = [
286         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
287     ]
288     _order = 'name desc'
289
290     # Form filling
291     def unlink(self, cr, uid, ids, context=None):
292         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
293         unlink_ids = []
294         for s in sale_orders:
295             if s['state'] in ['draft', 'cancel']:
296                 unlink_ids.append(s['id'])
297             else:
298                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
299
300         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
301
302     def onchange_shop_id(self, cr, uid, ids, shop_id):
303         v = {}
304         if shop_id:
305             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
306             v['project_id'] = shop.project_id.id
307             # Que faire si le client a une pricelist a lui ?
308             if shop.pricelist_id.id:
309                 v['pricelist_id'] = shop.pricelist_id.id
310         return {'value': v}
311
312     def action_cancel_draft(self, cr, uid, ids, context=None):
313         if not len(ids):
314             return False
315         cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
316         line_ids = map(lambda x: x[0], cr.fetchall())
317         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
318         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
319         wf_service = netsvc.LocalService("workflow")
320         for inv_id in ids:
321             # Deleting the existing instance of workflow for SO
322             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
323             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
324             self.action_cancel_draft_send_note(cr, uid, ids, context=context)
325         return True
326
327     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328         if (not pricelist_id) or (not order_lines):
329             return {}
330         warning = {
331             'title': _('Pricelist Warning!'),
332             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333         }
334         return {'warning': warning}
335
336     def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
337         if not order_id:
338             return {}
339         val = {}
340         if not invoice_id:
341             val['partner_invoice_id'] = order_id
342         if not shipping_id:
343             val['partner_shipping_id'] = order_id
344         return {'value': val}
345
346     def onchange_partner_id(self, cr, uid, ids, part):
347         if not part:
348             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
349
350         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
351         part = self.pool.get('res.partner').browse(cr, uid, part)
352         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
353         payment_term = part.property_payment_term and part.property_payment_term.id or False
354         fiscal_position = part.property_account_position and part.property_account_position.id or False
355         dedicated_salesman = part.user_id and part.user_id.id or uid
356         val = {
357             'partner_invoice_id': addr['invoice'],
358             'partner_shipping_id': addr['delivery'],
359             'payment_term': payment_term,
360             'fiscal_position': fiscal_position,
361             'user_id': dedicated_salesman,
362         }
363         if pricelist:
364             val['pricelist_id'] = pricelist
365         return {'value': val}
366
367     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
368         if not policy:
369             return {}
370         inv_qty = 'order'
371         if policy == 'prepaid':
372             inv_qty = 'order'
373         elif policy == 'picking':
374             inv_qty = 'procurement'
375         return {'value': {'invoice_quantity': inv_qty}}
376
377     def write(self, cr, uid, ids, vals, context=None):
378         if vals.get('order_policy', False):
379             if vals['order_policy'] == 'prepaid':
380                 vals.update({'invoice_quantity': 'order'})
381             elif vals['order_policy'] == 'picking':
382                 vals.update({'invoice_quantity': 'procurement'})
383         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
384
385     def create(self, cr, uid, vals, context=None):
386         if vals.get('order_policy', False):
387             if vals['order_policy'] == 'prepaid':
388                 vals.update({'invoice_quantity': 'order'})
389             if vals['order_policy'] == 'picking':
390                 vals.update({'invoice_quantity': 'procurement'})
391         order =  super(sale_order, self).create(cr, uid, vals, context=context)
392         if order:
393             self.create_send_note(cr, uid, [order], context=context)
394         return order
395
396     def button_dummy(self, cr, uid, ids, context=None):
397         return True
398
399     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
400     #        can be removed after 6.1.
401     def _inv_get(self, cr, uid, order, context=None):
402         return {}
403
404     def _prepare_invoice(self, cr, uid, order, lines, context=None):
405         """Prepare the dict of values to create the new invoice for a
406            sale order. This method may be overridden to implement custom
407            invoice generation (making sure to call super() to establish
408            a clean extension chain).
409
410            :param browse_record order: sale.order record to invoice
411            :param list(int) line: list of invoice line IDs that must be
412                                   attached to the invoice
413            :return: dict of value to create() the invoice
414         """
415         if context is None:
416             context = {}
417         journal_ids = self.pool.get('account.journal').search(cr, uid,
418             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
419             limit=1)
420         if not journal_ids:
421             raise osv.except_osv(_('Error!'),
422                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
423
424         invoice_vals = {
425             'name': order.client_order_ref or '',
426             'origin': order.name,
427             'type': 'out_invoice',
428             'reference': order.client_order_ref or order.name,
429             'account_id': order.partner_id.property_account_receivable.id,
430             'partner_id': order.partner_id.id,
431             'journal_id': journal_ids[0],
432             'invoice_line': [(6, 0, lines)],
433             'currency_id': order.pricelist_id.currency_id.id,
434             'comment': order.note,
435             'payment_term': order.payment_term and order.payment_term.id or False,
436             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
437             'date_invoice': context.get('date_invoice', False),
438             'company_id': order.company_id.id,
439             'user_id': order.user_id and order.user_id.id or False
440         }
441
442         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
443         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
444
445         return invoice_vals
446
447     def _make_invoice(self, cr, uid, order, lines, context=None):
448         inv_obj = self.pool.get('account.invoice')
449         obj_invoice_line = self.pool.get('account.invoice.line')
450         if context is None:
451             context = {}
452         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
453         from_line_invoice_ids = []
454         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
455             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
456                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
457                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
458         for preinv in order.invoice_ids:
459             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
460                 for preline in preinv.invoice_line:
461                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
462                     lines.append(inv_line_id)
463         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
464         inv_id = inv_obj.create(cr, uid, inv, context=context)
465         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
466         if data.get('value', False):
467             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
468         inv_obj.button_compute(cr, uid, [inv_id])
469         return inv_id
470
471     def print_quotation(self, cr, uid, ids, context=None):
472         '''
473         This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
474         '''
475         assert len(ids) == 1, 'This option should only be used for a single id at a time'
476         wf_service = netsvc.LocalService("workflow")
477         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
478         datas = {
479                  'model': 'sale.order',
480                  'ids': ids,
481                  'form': self.read(cr, uid, ids[0], context=context),
482         }
483         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
484
485     def manual_invoice(self, cr, uid, ids, context=None):
486         """ create invoices for the given sale orders (ids), and open the form
487             view of one of the newly created invoices
488         """
489         mod_obj = self.pool.get('ir.model.data')
490         wf_service = netsvc.LocalService("workflow")
491
492         # create invoices through the sale orders' workflow
493         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
494         for id in ids:
495             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
496         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
497         # determine newly created invoices
498         new_inv_ids = list(inv_ids1 - inv_ids0)
499
500         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
501         res_id = res and res[1] or False,
502
503         return {
504             'name': _('Customer Invoices'),
505             'view_type': 'form',
506             'view_mode': 'form',
507             'view_id': [res_id],
508             'res_model': 'account.invoice',
509             'context': "{'type':'out_invoice'}",
510             'type': 'ir.actions.act_window',
511             'nodestroy': True,
512             'target': 'current',
513             'res_id': new_inv_ids and new_inv_ids[0] or False,
514         }
515
516     def action_view_invoice(self, cr, uid, ids, context=None):
517         '''
518         This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
519         '''
520         mod_obj = self.pool.get('ir.model.data')
521         result = {
522             'name': _('Cutomer Invoice'),
523             'view_type': 'form',
524             'res_model': 'account.invoice',
525             'context': "{'type':'out_invoice', 'journal_type': 'sale'}",
526             'type': 'ir.actions.act_window',
527             'nodestroy': True,
528             'target': 'current',
529         }
530         #compute the number of invoices to display
531         inv_ids = []
532         for so in self.browse(cr, uid, ids, context=context):
533             inv_ids += [invoice.id for invoice in so.invoice_ids]
534         #choose the view_mode accordingly
535         if len(inv_ids)>1:
536             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree')
537             result.update({
538                 'view_mode': 'tree,form',
539                 'res_id': inv_ids or False
540             })
541         else:
542             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
543             result.update({
544                 'view_mode': 'form',
545                 'res_id': inv_ids and inv_ids[0] or False,
546             })
547         result.update(view_id = res and res[1] or False)
548         return result
549
550
551     def action_view_delivery(self, cr, uid, ids, context=None):
552         '''
553         This function returns an action that display existing delivery orders of given sale order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
554         '''
555         mod_obj = self.pool.get('ir.model.data')
556         result = {
557             'name': _('Delivery Order'),
558             'view_type': 'form',
559             'res_model': 'stock.picking',
560             'context': "{'type':'out'}",
561             'type': 'ir.actions.act_window',
562             'nodestroy': True,
563             'target': 'current',
564         }
565         #compute the number of delivery orders to display
566         pick_ids = []
567         for so in self.browse(cr, uid, ids, context=context):
568             pick_ids += [picking.id for picking in so.picking_ids]
569         #choose the view_mode accordingly
570         if len(pick_ids) > 1:
571             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_tree')
572             result.update({
573                 'view_mode': 'tree,form',
574                 'res_id': pick_ids or False
575             })
576         else:
577             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
578             result.update({
579                 'view_mode': 'form',
580                 'res_id': pick_ids and pick_ids[0] or False,
581             })
582         result.update(view_id = res and res[1] or False)
583         return result
584
585     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
586         res = False
587         invoices = {}
588         invoice_ids = []
589         picking_obj = self.pool.get('stock.picking')
590         invoice = self.pool.get('account.invoice')
591         obj_sale_order_line = self.pool.get('sale.order.line')
592         partner_currency = {}
593         if context is None:
594             context = {}
595         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
596         # last day of the last month as invoice date
597         if date_inv:
598             context['date_inv'] = date_inv
599         for o in self.browse(cr, uid, ids, context=context):
600             currency_id = o.pricelist_id.currency_id.id
601             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
602                 raise osv.except_osv(
603                     _('Error!'),
604                     _('You cannot group sales having different currencies for the same partner.'))
605
606             partner_currency[o.partner_id.id] = currency_id
607             lines = []
608             for line in o.order_line:
609                 if line.invoiced:
610                     continue
611                 elif (line.state in states):
612                     lines.append(line.id)
613             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
614             if created_lines:
615                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
616         if not invoices:
617             for o in self.browse(cr, uid, ids, context=context):
618                 for i in o.invoice_ids:
619                     if i.state == 'draft':
620                         return i.id
621         for val in invoices.values():
622             if grouped:
623                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
624                 invoice_ref = ''
625                 for o, l in val:
626                     invoice_ref += o.name + '|'
627                     self.write(cr, uid, [o.id], {'state': 'progress'})
628                     if o.order_policy == 'picking':
629                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
630                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
631                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
632             else:
633                 for order, il in val:
634                     res = self._make_invoice(cr, uid, order, il, context=context)
635                     invoice_ids.append(res)
636                     self.write(cr, uid, [order.id], {'state': 'progress'})
637                     if order.order_policy == 'picking':
638                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
639                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
640         if res:
641             self.invoice_send_note(cr, uid, ids, res, context)
642         return res
643
644     def action_invoice_cancel(self, cr, uid, ids, context=None):
645         if context is None:
646             context = {}
647         for sale in self.browse(cr, uid, ids, context=context):
648             for line in sale.order_line:
649                 #
650                 # Check if the line is invoiced (has asociated invoice
651                 # lines from non-cancelled invoices).
652                 #
653                 invoiced = False
654                 for iline in line.invoice_lines:
655                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
656                         invoiced = True
657                         break
658                 # Update the line (only when needed)
659                 if line.invoiced != invoiced:
660                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
661         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
662         return True
663
664     def action_invoice_end(self, cr, uid, ids, context=None):
665         for order in self.browse(cr, uid, ids, context=context):
666             #
667             # Update the sale order lines state (and invoiced flag).
668             #
669             for line in order.order_line:
670                 vals = {}
671                 #
672                 # Check if the line is invoiced (has asociated invoice
673                 # lines from non-cancelled invoices).
674                 #
675                 invoiced = False
676                 for iline in line.invoice_lines:
677                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
678                         invoiced = True
679                         break
680                 if line.invoiced != invoiced:
681                     vals['invoiced'] = invoiced
682                 # If the line was in exception state, now it gets confirmed.
683                 if line.state == 'exception':
684                     vals['state'] = 'confirmed'
685                 # Update the line (only when needed).
686                 if vals:
687                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
688             #
689             # Update the sales order state.
690             #
691             if order.state == 'invoice_except':
692                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
693         return True
694
695     def action_cancel(self, cr, uid, ids, context=None):
696         wf_service = netsvc.LocalService("workflow")
697         if context is None:
698             context = {}
699         sale_order_line_obj = self.pool.get('sale.order.line')
700         proc_obj = self.pool.get('procurement.order')
701         for sale in self.browse(cr, uid, ids, context=context):
702             for pick in sale.picking_ids:
703                 if pick.state not in ('draft', 'cancel'):
704                     raise osv.except_osv(
705                         _('Cannot cancel sales order!'),
706                         _('You must first cancel all delivery order(s) attached to this sales order.'))
707                 if pick.state == 'cancel':
708                     for mov in pick.move_lines:
709                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
710                         if proc_ids:
711                             for proc in proc_ids:
712                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
713             for r in self.read(cr, uid, ids, ['picking_ids']):
714                 for pick in r['picking_ids']:
715                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
716             for inv in sale.invoice_ids:
717                 if inv.state not in ('draft', 'cancel'):
718                     raise osv.except_osv(
719                         _('Cannot cancel this sales order!'),
720                         _('First cancel all invoices attached to this sales order.'))
721             for r in self.read(cr, uid, ids, ['invoice_ids']):
722                 for inv in r['invoice_ids']:
723                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
724             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
725                     {'state': 'cancel'})
726             self.cancel_send_note(cr, uid, [sale.id], context=None)
727         self.write(cr, uid, ids, {'state': 'cancel'})
728         return True
729
730     def action_button_confirm(self, cr, uid, ids, context=None):
731         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
732         wf_service = netsvc.LocalService('workflow')
733         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
734
735         # redisplay the record as a sale order
736         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
737         view_id = view_ref and view_ref[1] or False,
738         return {
739             'type': 'ir.actions.act_window',
740             'name': _('Sales Order'),
741             'res_model': 'sale.order',
742             'res_id': ids[0],
743             'view_type': 'form',
744             'view_mode': 'form',
745             'view_id': view_id,
746             'target': 'current',
747             'nodestroy': True,
748         }
749
750     def action_wait(self, cr, uid, ids, context=None):
751         for o in self.browse(cr, uid, ids):
752             if not o.order_line:
753                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
754             if (o.order_policy == 'manual'):
755                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
756             else:
757                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
758             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
759             self.confirm_send_note(cr, uid, ids, context)
760         return True
761
762     def action_quotation_send(self, cr, uid, ids, context=None):
763         '''
764         This function opens a window to compose an email, with the edi sale template message loaded by default
765         '''
766         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
767         mod_obj = self.pool.get('ir.model.data')
768         template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
769         template_id = template and template[1] or False
770         res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
771         res_id = res and res[1] or False
772         ctx = dict(context, active_model='sale.order', active_id=ids[0])
773         ctx.update({'mail.compose.template_id': template_id})
774         return {
775             'view_type': 'form',
776             'view_mode': 'form',
777             'res_model': 'mail.compose.message',
778             'views': [(res_id,'form')],
779             'view_id': res_id,
780             'type': 'ir.actions.act_window',
781             'target': 'new',
782             'context': ctx,
783             'nodestroy': True,
784         }
785
786     def procurement_lines_get(self, cr, uid, ids, *args):
787         res = []
788         for order in self.browse(cr, uid, ids, context={}):
789             for line in order.order_line:
790                 if line.procurement_id:
791                     res.append(line.procurement_id.id)
792         return res
793
794     # if mode == 'finished':
795     #   returns True if all lines are done, False otherwise
796     # if mode == 'canceled':
797     #   returns True if there is at least one canceled line, False otherwise
798     def test_state(self, cr, uid, ids, mode, *args):
799         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
800         finished = True
801         canceled = False
802         notcanceled = False
803         write_done_ids = []
804         write_cancel_ids = []
805         for order in self.browse(cr, uid, ids, context={}):
806             for line in order.order_line:
807                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
808                     if line.state != 'done':
809                         write_done_ids.append(line.id)
810                 else:
811                     finished = False
812                 if line.procurement_id:
813                     if (line.procurement_id.state == 'cancel'):
814                         canceled = True
815                         if line.state != 'exception':
816                             write_cancel_ids.append(line.id)
817                     else:
818                         notcanceled = True
819         if write_done_ids:
820             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
821         if write_cancel_ids:
822             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
823
824         if mode == 'finished':
825             return finished
826         elif mode == 'canceled':
827             return canceled
828             if notcanceled:
829                 return False
830             return canceled
831
832     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
833         return {
834             'name': line.name.split('\n')[0],
835             'origin': order.name,
836             'date_planned': date_planned,
837             'product_id': line.product_id.id,
838             'product_qty': line.product_uom_qty,
839             'product_uom': line.product_uom.id,
840             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
841                     or line.product_uom_qty,
842             'product_uos': (line.product_uos and line.product_uos.id)\
843                     or line.product_uom.id,
844             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
845             'procure_method': line.type,
846             'move_id': move_id,
847             'company_id': order.company_id.id,
848             'note': '\n'.join(line.name.split('\n')[1:]),
849             'property_ids': [(6, 0, [x.id for x in line.property_ids])]
850         }
851
852     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
853         location_id = order.shop_id.warehouse_id.lot_stock_id.id
854         output_id = order.shop_id.warehouse_id.lot_output_id.id
855         return {
856             'name': line.name.split('\n')[0][:250],
857             'picking_id': picking_id,
858             'product_id': line.product_id.id,
859             'date': date_planned,
860             'date_expected': date_planned,
861             'product_qty': line.product_uom_qty,
862             'product_uom': line.product_uom.id,
863             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
864             'product_uos': (line.product_uos and line.product_uos.id)\
865                     or line.product_uom.id,
866             'product_packaging': line.product_packaging.id,
867             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
868             'location_id': location_id,
869             'location_dest_id': output_id,
870             'sale_line_id': line.id,
871             'tracking_id': False,
872             'state': 'draft',
873             #'state': 'waiting',
874             'note': '\n'.join(line.name.split('\n')[1:]),
875             'company_id': order.company_id.id,
876             'price_unit': line.product_id.standard_price or 0.0
877         }
878
879     def _prepare_order_picking(self, cr, uid, order, context=None):
880         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
881         return {
882             'name': pick_name,
883             'origin': order.name,
884             'date': order.date_order,
885             'type': 'out',
886             'state': 'auto',
887             'move_type': order.picking_policy,
888             'sale_id': order.id,
889             'partner_id': order.partner_shipping_id.id,
890             'note': order.note,
891             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
892             'company_id': order.company_id.id,
893         }
894
895     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
896         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
897         """
898         Define ship_recreate for process after shipping exception
899         param order: sale order to which the order lines belong
900         param line: sale order line records to procure
901         param move_id: the ID of stock move
902         param proc_id: the ID of procurement
903         """
904         move_obj = self.pool.get('stock.move')
905         if order.state == 'shipping_except':
906             for pick in order.picking_ids:
907                 for move in pick.move_lines:
908                     if move.state == 'cancel':
909                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
910                         if mov_ids:
911                             for mov in move_obj.browse(cr, uid, mov_ids):
912                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
913                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
914                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
915         return True
916
917     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
918         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
919         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
920         return date_planned
921
922     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
923         """Create the required procurements to supply sale order lines, also connecting
924         the procurements to appropriate stock moves in order to bring the goods to the
925         sale order's requested location.
926
927         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
928         a standard outgoing picking will be created to wrap the stock moves, as returned
929         by :meth:`~._prepare_order_picking`.
930
931         Modules that wish to customize the procurements or partition the stock moves over
932         multiple stock pickings may override this method and call ``super()`` with
933         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
934
935         :param browse_record order: sale order to which the order lines belong
936         :param list(browse_record) order_lines: sale order line records to procure
937         :param int picking_id: optional ID of a stock picking to which the created stock moves
938                                will be added. A new picking will be created if ommitted.
939         :return: True
940         """
941         move_obj = self.pool.get('stock.move')
942         picking_obj = self.pool.get('stock.picking')
943         procurement_obj = self.pool.get('procurement.order')
944         proc_ids = []
945
946         for line in order_lines:
947             if line.state == 'done':
948                 continue
949
950             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
951
952             if line.product_id:
953                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
954                     if not picking_id:
955                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
956                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
957                 else:
958                     # a service has no stock move
959                     move_id = False
960
961                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
962                 proc_ids.append(proc_id)
963                 line.write({'procurement_id': proc_id})
964                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
965
966         wf_service = netsvc.LocalService("workflow")
967         if picking_id:
968             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
969             self.delivery_send_note(cr, uid, [order.id], picking_id, context)
970
971
972         for proc_id in proc_ids:
973             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
974
975         val = {}
976         if order.state == 'shipping_except':
977             val['state'] = 'progress'
978             val['shipped'] = False
979
980             if (order.order_policy == 'manual'):
981                 for line in order.order_line:
982                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
983                         val['state'] = 'manual'
984                         break
985         order.write(val)
986         return True
987
988     def action_ship_create(self, cr, uid, ids, context=None):
989         for order in self.browse(cr, uid, ids, context=context):
990             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
991         return True
992
993     def action_ship_end(self, cr, uid, ids, context=None):
994         for order in self.browse(cr, uid, ids, context=context):
995             val = {'shipped': True}
996             if order.state == 'shipping_except':
997                 val['state'] = 'progress'
998                 if (order.order_policy == 'manual'):
999                     for line in order.order_line:
1000                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
1001                             val['state'] = 'manual'
1002                             break
1003             for line in order.order_line:
1004                 towrite = []
1005                 if line.state == 'exception':
1006                     towrite.append(line.id)
1007                 if towrite:
1008                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
1009             res = self.write(cr, uid, [order.id], val)
1010             if res:
1011                 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1012         return True
1013
1014     def has_stockable_products(self, cr, uid, ids, *args):
1015         for order in self.browse(cr, uid, ids):
1016             for order_line in order.order_line:
1017                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1018                     return True
1019         return False
1020
1021     # ------------------------------------------------
1022     # OpenChatter methods and notifications
1023     # ------------------------------------------------
1024
1025     def needaction_domain_get(self, cr, uid, ids, context=None):
1026         return [('state', '=', 'draft'), ('user_id','=',uid)]
1027
1028     def create_send_note(self, cr, uid, ids, context=None):
1029         for obj in self.browse(cr, uid, ids, context=context):
1030             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
1031
1032     def confirm_send_note(self, cr, uid, ids, context=None):
1033         for obj in self.browse(cr, uid, ids, context=context):
1034             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
1035
1036     def cancel_send_note(self, cr, uid, ids, context=None):
1037         for obj in self.browse(cr, uid, ids, context=context):
1038             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1039
1040     def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1041         for order in self.browse(cr, uid, ids, context=context):
1042             for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1043                 # convert datetime field to a datetime, using server format, then
1044                 # convert it to the user TZ and re-render it with %Z to add the timezone
1045                 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1046                 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1047                 self.message_post(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
1048
1049     def delivery_end_send_note(self, cr, uid, ids, context=None):
1050         self.message_post(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1051
1052     def invoice_paid_send_note(self, cr, uid, ids, context=None):
1053         self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1054
1055     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1056         for order in self.browse(cr, uid, ids, context=context):
1057             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1058                 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
1059
1060     def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1061         return self.message_post(cr, uid, ids, body=_('Sale order set to draft.'), context=context)
1062
1063
1064 sale_order()
1065
1066 # TODO add a field price_unit_uos
1067 # - update it on change product and unit price
1068 # - use it in report if there is a uos
1069 class sale_order_line(osv.osv):
1070
1071     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1072         tax_obj = self.pool.get('account.tax')
1073         cur_obj = self.pool.get('res.currency')
1074         res = {}
1075         if context is None:
1076             context = {}
1077         for line in self.browse(cr, uid, ids, context=context):
1078             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1079             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1080             cur = line.order_id.pricelist_id.currency_id
1081             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1082         return res
1083
1084     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1085         res = {}
1086         for line in self.browse(cr, uid, ids, context=context):
1087             try:
1088                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1089             except:
1090                 res[line.id] = 1
1091         return res
1092
1093     def _get_uom_id(self, cr, uid, *args):
1094         try:
1095             proxy = self.pool.get('ir.model.data')
1096             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1097             return result[1]
1098         except Exception, ex:
1099             return False
1100
1101     _name = 'sale.order.line'
1102     _description = 'Sales Order Line'
1103     _columns = {
1104         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1105         'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1106         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1107         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
1108         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1109         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1110         'invoiced': fields.boolean('Invoiced', readonly=True),
1111         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1112         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
1113         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
1114         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1115         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1116             help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
1117         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1118         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1119         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1120         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1121         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1122         'product_uos': fields.many2one('product.uom', 'Product UoS'),
1123         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1124         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1125         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
1126         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1127         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1128         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1129                 help='* The \'Draft\' state is set when the related sales order in draft state. \
1130                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1131                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
1132                     \n* The \'Done\' state is set when the sales order line has been picked. \
1133                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1134         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1135         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1136         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1137     }
1138     _order = 'sequence, id'
1139     _defaults = {
1140         'product_uom' : _get_uom_id,
1141         'discount': 0.0,
1142         'delay': 0.0,
1143         'product_uom_qty': 1,
1144         'product_uos_qty': 1,
1145         'sequence': 10,
1146         'invoiced': 0,
1147         'state': 'draft',
1148         'type': 'make_to_stock',
1149         'product_packaging': False,
1150         'price_unit': 0.0,
1151     }
1152
1153     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1154         """Prepare the dict of values to create the new invoice line for a
1155            sale order line. This method may be overridden to implement custom
1156            invoice generation (making sure to call super() to establish
1157            a clean extension chain).
1158
1159            :param browse_record line: sale.order.line record to invoice
1160            :param int account_id: optional ID of a G/L account to force
1161                (this is used for returning products including service)
1162            :return: dict of values to create() the invoice line
1163         """
1164
1165         def _get_line_qty(line):
1166             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1167                 if line.product_uos:
1168                     return line.product_uos_qty or 0.0
1169                 return line.product_uom_qty
1170             else:
1171                 return self.pool.get('procurement.order').quantity_get(cr, uid,
1172                         line.procurement_id.id, context=context)
1173
1174         def _get_line_uom(line):
1175             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1176                 if line.product_uos:
1177                     return line.product_uos.id
1178                 return line.product_uom.id
1179             else:
1180                 return self.pool.get('procurement.order').uom_get(cr, uid,
1181                         line.procurement_id.id, context=context)
1182
1183         if not line.invoiced:
1184             if not account_id:
1185                 if line.product_id:
1186                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1187                     if not account_id:
1188                         account_id = line.product_id.categ_id.property_account_income_categ.id
1189                     if not account_id:
1190                         raise osv.except_osv(_('Error!'),
1191                                 _('Please define income account for this product: "%s" (id:%d).') % \
1192                                     (line.product_id.name, line.product_id.id,))
1193                 else:
1194                     prop = self.pool.get('ir.property').get(cr, uid,
1195                             'property_account_income_categ', 'product.category',
1196                             context=context)
1197                     account_id = prop and prop.id or False
1198             uosqty = _get_line_qty(line)
1199             uos_id = _get_line_uom(line)
1200             pu = 0.0
1201             if uosqty:
1202                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1203                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
1204             fpos = line.order_id.fiscal_position or False
1205             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1206             if not account_id:
1207                 raise osv.except_osv(_('Error!'),
1208                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
1209             return {
1210                 'name': line.name,
1211                 'origin': line.order_id.name,
1212                 'account_id': account_id,
1213                 'price_unit': pu,
1214                 'quantity': uosqty,
1215                 'discount': line.discount,
1216                 'uos_id': uos_id,
1217                 'product_id': line.product_id.id or False,
1218                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1219                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1220             }
1221
1222         return False
1223
1224     def invoice_line_create(self, cr, uid, ids, context=None):
1225         if context is None:
1226             context = {}
1227
1228         create_ids = []
1229         sales = set()
1230         for line in self.browse(cr, uid, ids, context=context):
1231             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1232             if vals:
1233                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1234                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1235                 self.write(cr, uid, [line.id], {'invoiced': True})
1236                 sales.add(line.order_id.id)
1237                 create_ids.append(inv_id)
1238         # Trigger workflow events
1239         wf_service = netsvc.LocalService("workflow")
1240         for sale_id in sales:
1241             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1242         return create_ids
1243
1244     def button_cancel(self, cr, uid, ids, context=None):
1245         for line in self.browse(cr, uid, ids, context=context):
1246             if line.invoiced:
1247                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
1248             for move_line in line.move_ids:
1249                 if move_line.state != 'cancel':
1250                     raise osv.except_osv(
1251                             _('Cannot cancel sales order line!'),
1252                             _('You must first cancel stock moves attached to this sales order line.'))
1253         return self.write(cr, uid, ids, {'state': 'cancel'})
1254
1255     def button_confirm(self, cr, uid, ids, context=None):
1256         return self.write(cr, uid, ids, {'state': 'confirmed'})
1257
1258     def button_done(self, cr, uid, ids, context=None):
1259         wf_service = netsvc.LocalService("workflow")
1260         res = self.write(cr, uid, ids, {'state': 'done'})
1261         for line in self.browse(cr, uid, ids, context=context):
1262             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1263         return res
1264
1265     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1266         product_obj = self.pool.get('product.product')
1267         if not product_id:
1268             return {'value': {'product_uom': product_uos,
1269                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1270
1271         product = product_obj.browse(cr, uid, product_id)
1272         value = {
1273             'product_uom': product.uom_id.id,
1274         }
1275         # FIXME must depend on uos/uom of the product and not only of the coeff.
1276         try:
1277             value.update({
1278                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1279                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1280             })
1281         except ZeroDivisionError:
1282             pass
1283         return {'value': value}
1284
1285     def copy_data(self, cr, uid, id, default=None, context=None):
1286         if not default:
1287             default = {}
1288         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1289         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1290
1291     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1292                                    partner_id=False, packaging=False, flag=False, context=None):
1293         if not product:
1294             return {'value': {'product_packaging': False}}
1295         product_obj = self.pool.get('product.product')
1296         product_uom_obj = self.pool.get('product.uom')
1297         pack_obj = self.pool.get('product.packaging')
1298         warning = {}
1299         result = {}
1300         warning_msgs = ''
1301         if flag:
1302             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1303                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1304                     packaging=packaging, flag=False, context=context)
1305             warning_msgs = res.get('warning') and res['warning']['message']
1306
1307         products = product_obj.browse(cr, uid, product, context=context)
1308         if not products.packaging:
1309             packaging = result['product_packaging'] = False
1310         elif not packaging and products.packaging and not flag:
1311             packaging = products.packaging[0].id
1312             result['product_packaging'] = packaging
1313
1314         if packaging:
1315             default_uom = products.uom_id and products.uom_id.id
1316             pack = pack_obj.browse(cr, uid, packaging, context=context)
1317             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1318 #            qty = qty - qty % q + q
1319             if qty and (q and not (qty % q) == 0):
1320                 ean = pack.ean or _('(n/a)')
1321                 qty_pack = pack.qty
1322                 type_ul = pack.ul
1323                 if not warning_msgs:
1324                     warn_msg = _("You selected a quantity of %d Units.\n"
1325                                 "But it's not compatible with the selected packaging.\n"
1326                                 "Here is a proposition of quantities according to the packaging:\n"
1327                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1328                                     (qty, ean, qty_pack, type_ul.name)
1329                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1330                 warning = {
1331                        'title': _('Configuration Error!'),
1332                        'message': warning_msgs
1333                 }
1334             result['product_uom_qty'] = qty
1335
1336         return {'value': result, 'warning': warning}
1337
1338     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1339             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1340             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1341         context = context or {}
1342         lang = lang or context.get('lang',False)
1343         if not  partner_id:
1344             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1345         warning = {}
1346         product_uom_obj = self.pool.get('product.uom')
1347         partner_obj = self.pool.get('res.partner')
1348         product_obj = self.pool.get('product.product')
1349         context = {'lang': lang, 'partner_id': partner_id}
1350         if partner_id:
1351             lang = partner_obj.browse(cr, uid, partner_id).lang
1352         context_partner = {'lang': lang, 'partner_id': partner_id}
1353
1354         if not product:
1355             return {'value': {'th_weight': 0, 'product_packaging': False,
1356                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1357                    'product_uos': []}}
1358         if not date_order:
1359             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1360
1361         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1362         result = res.get('value', {})
1363         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1364         product_obj = product_obj.browse(cr, uid, product, context=context)
1365
1366         uom2 = False
1367         if uom:
1368             uom2 = product_uom_obj.browse(cr, uid, uom)
1369             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1370                 uom = False
1371         if uos:
1372             if product_obj.uos_id:
1373                 uos2 = product_uom_obj.browse(cr, uid, uos)
1374                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1375                     uos = False
1376             else:
1377                 uos = False
1378         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1379         if update_tax: #The quantity only have changed
1380             result['delay'] = (product_obj.sale_delay or 0.0)
1381             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1382             result.update({'type': product_obj.procure_method})
1383
1384         if not flag:
1385             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1386             if product_obj.description_sale:
1387                 result['name'] += '\n'+product_obj.description_sale
1388         domain = {}
1389         if (not uom) and (not uos):
1390             result['product_uom'] = product_obj.uom_id.id
1391             if product_obj.uos_id:
1392                 result['product_uos'] = product_obj.uos_id.id
1393                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1394                 uos_category_id = product_obj.uos_id.category_id.id
1395             else:
1396                 result['product_uos'] = False
1397                 result['product_uos_qty'] = qty
1398                 uos_category_id = False
1399             result['th_weight'] = qty * product_obj.weight
1400             domain = {'product_uom':
1401                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1402                         'product_uos':
1403                         [('category_id', '=', uos_category_id)]}
1404         elif uos and not uom: # only happens if uom is False
1405             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1406             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1407             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1408         elif uom: # whether uos is set or not
1409             default_uom = product_obj.uom_id and product_obj.uom_id.id
1410             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1411             if product_obj.uos_id:
1412                 result['product_uos'] = product_obj.uos_id.id
1413                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1414             else:
1415                 result['product_uos'] = False
1416                 result['product_uos_qty'] = qty
1417             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1418
1419         if not uom2:
1420             uom2 = product_obj.uom_id
1421         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1422         if (product_obj.type=='product') and int(compare_qty) == -1 \
1423           and (product_obj.procure_method=='make_to_stock'):
1424             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1425                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1426                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1427                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1428             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1429         # get unit price
1430
1431         if not pricelist:
1432             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1433                     'Please set one before choosing a product.')
1434             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1435         else:
1436             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1437                     product, qty or 1.0, partner_id, {
1438                         'uom': uom or result.get('product_uom'),
1439                         'date': date_order,
1440                         })[pricelist]
1441             if price is False:
1442                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1443                         "You have to change either the product, the quantity or the pricelist.")
1444
1445                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1446             else:
1447                 result.update({'price_unit': price})
1448         if warning_msgs:
1449             warning = {
1450                        'title': _('Configuration Error!'),
1451                        'message' : warning_msgs
1452                     }
1453         return {'value': result, 'domain': domain, 'warning': warning}
1454
1455     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1456             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1457             lang=False, update_tax=True, date_order=False, context=None):
1458         context = context or {}
1459         lang = lang or ('lang' in context and context['lang'])
1460         if not uom:
1461             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1462         return self.product_id_change(cursor, user, ids, pricelist, product,
1463                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1464                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1465                 date_order=date_order, context=context)
1466
1467     def unlink(self, cr, uid, ids, context=None):
1468         if context is None:
1469             context = {}
1470         """Allows to delete sales order lines in draft,cancel states"""
1471         for rec in self.browse(cr, uid, ids, context=context):
1472             if rec.state not in ['draft', 'cancel']:
1473                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1474         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1475
1476 sale_order_line()
1477
1478 class mail_message(osv.osv):
1479     _inherit = 'mail.message'
1480
1481     def _postprocess_sent_message(self, cr, uid, message, context=None):
1482         if message.model == 'sale.order':
1483             wf_service = netsvc.LocalService("workflow")
1484             wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1485         return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1486
1487 mail_message()
1488
1489 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: