[FIX] sale : ValueError: Non-db action dictionaries should provide either multiple...
[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 = ['ir.needaction_mixin', 'mail.thread']
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         act_obj = self.pool.get('ir.actions.act_window')
557
558         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
559         id = result and result[1] or False
560         result = act_obj.read(cr, uid, [id], context=context)[0]
561         #compute the number of delivery orders to display
562         pick_ids = []
563         for so in self.browse(cr, uid, ids, context=context):
564             pick_ids += [picking.id for picking in so.picking_ids]
565         #choose the view_mode accordingly
566         if len(pick_ids) > 1:
567             result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
568         else:
569             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
570             result['views'] = [(res and res[1] or False, 'form')]
571             result['res_id'] = pick_ids and pick_ids[0] or False
572         return result
573
574     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
575         res = False
576         invoices = {}
577         invoice_ids = []
578         picking_obj = self.pool.get('stock.picking')
579         invoice = self.pool.get('account.invoice')
580         obj_sale_order_line = self.pool.get('sale.order.line')
581         partner_currency = {}
582         if context is None:
583             context = {}
584         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
585         # last day of the last month as invoice date
586         if date_inv:
587             context['date_inv'] = date_inv
588         for o in self.browse(cr, uid, ids, context=context):
589             currency_id = o.pricelist_id.currency_id.id
590             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
591                 raise osv.except_osv(
592                     _('Error!'),
593                     _('You cannot group sales having different currencies for the same partner.'))
594
595             partner_currency[o.partner_id.id] = currency_id
596             lines = []
597             for line in o.order_line:
598                 if line.invoiced:
599                     continue
600                 elif (line.state in states):
601                     lines.append(line.id)
602             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
603             if created_lines:
604                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
605         if not invoices:
606             for o in self.browse(cr, uid, ids, context=context):
607                 for i in o.invoice_ids:
608                     if i.state == 'draft':
609                         return i.id
610         for val in invoices.values():
611             if grouped:
612                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
613                 invoice_ref = ''
614                 for o, l in val:
615                     invoice_ref += o.name + '|'
616                     self.write(cr, uid, [o.id], {'state': 'progress'})
617                     if o.order_policy == 'picking':
618                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
619                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
620                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
621             else:
622                 for order, il in val:
623                     res = self._make_invoice(cr, uid, order, il, context=context)
624                     invoice_ids.append(res)
625                     self.write(cr, uid, [order.id], {'state': 'progress'})
626                     if order.order_policy == 'picking':
627                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
628                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
629         if res:
630             self.invoice_send_note(cr, uid, ids, res, context)
631         return res
632
633     def action_invoice_cancel(self, cr, uid, ids, context=None):
634         if context is None:
635             context = {}
636         for sale in self.browse(cr, uid, ids, context=context):
637             for line in sale.order_line:
638                 #
639                 # Check if the line is invoiced (has asociated invoice
640                 # lines from non-cancelled invoices).
641                 #
642                 invoiced = False
643                 for iline in line.invoice_lines:
644                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
645                         invoiced = True
646                         break
647                 # Update the line (only when needed)
648                 if line.invoiced != invoiced:
649                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
650         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
651         return True
652
653     def action_invoice_end(self, cr, uid, ids, context=None):
654         for order in self.browse(cr, uid, ids, context=context):
655             #
656             # Update the sale order lines state (and invoiced flag).
657             #
658             for line in order.order_line:
659                 vals = {}
660                 #
661                 # Check if the line is invoiced (has asociated invoice
662                 # lines from non-cancelled invoices).
663                 #
664                 invoiced = False
665                 for iline in line.invoice_lines:
666                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
667                         invoiced = True
668                         break
669                 if line.invoiced != invoiced:
670                     vals['invoiced'] = invoiced
671                 # If the line was in exception state, now it gets confirmed.
672                 if line.state == 'exception':
673                     vals['state'] = 'confirmed'
674                 # Update the line (only when needed).
675                 if vals:
676                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
677             #
678             # Update the sales order state.
679             #
680             if order.state == 'invoice_except':
681                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
682         return True
683
684     def action_cancel(self, cr, uid, ids, context=None):
685         wf_service = netsvc.LocalService("workflow")
686         if context is None:
687             context = {}
688         sale_order_line_obj = self.pool.get('sale.order.line')
689         proc_obj = self.pool.get('procurement.order')
690         for sale in self.browse(cr, uid, ids, context=context):
691             for pick in sale.picking_ids:
692                 if pick.state not in ('draft', 'cancel'):
693                     raise osv.except_osv(
694                         _('Cannot cancel sales order!'),
695                         _('You must first cancel all delivery order(s) attached to this sales order.'))
696                 if pick.state == 'cancel':
697                     for mov in pick.move_lines:
698                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
699                         if proc_ids:
700                             for proc in proc_ids:
701                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
702             for r in self.read(cr, uid, ids, ['picking_ids']):
703                 for pick in r['picking_ids']:
704                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
705             for inv in sale.invoice_ids:
706                 if inv.state not in ('draft', 'cancel'):
707                     raise osv.except_osv(
708                         _('Cannot cancel this sales order!'),
709                         _('First cancel all invoices attached to this sales order.'))
710             for r in self.read(cr, uid, ids, ['invoice_ids']):
711                 for inv in r['invoice_ids']:
712                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
713             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
714                     {'state': 'cancel'})
715             self.cancel_send_note(cr, uid, [sale.id], context=None)
716         self.write(cr, uid, ids, {'state': 'cancel'})
717         return True
718
719     def action_button_confirm(self, cr, uid, ids, context=None):
720         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
721         wf_service = netsvc.LocalService('workflow')
722         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
723
724         # redisplay the record as a sale order
725         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
726         view_id = view_ref and view_ref[1] or False,
727         return {
728             'type': 'ir.actions.act_window',
729             'name': _('Sales Order'),
730             'res_model': 'sale.order',
731             'res_id': ids[0],
732             'view_type': 'form',
733             'view_mode': 'form',
734             'view_id': view_id,
735             'target': 'current',
736             'nodestroy': True,
737         }
738
739     def action_wait(self, cr, uid, ids, context=None):
740         for o in self.browse(cr, uid, ids):
741             if not o.order_line:
742                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
743             if (o.order_policy == 'manual'):
744                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
745             else:
746                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
747             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
748             self.confirm_send_note(cr, uid, ids, context)
749         return True
750
751     def action_quotation_send(self, cr, uid, ids, context=None):
752         '''
753         This function opens a window to compose an email, with the edi sale template message loaded by default
754         '''
755         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
756         mod_obj = self.pool.get('ir.model.data')
757         template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
758         template_id = template and template[1] or False
759         res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
760         res_id = res and res[1] or False
761         ctx = dict(context, active_model='sale.order', active_id=ids[0])
762         ctx.update({'mail.compose.template_id': template_id})
763         return {
764             'view_type': 'form',
765             'view_mode': 'form',
766             'res_model': 'mail.compose.message',
767             'views': [(res_id,'form')],
768             'view_id': res_id,
769             'type': 'ir.actions.act_window',
770             'target': 'new',
771             'context': ctx,
772             'nodestroy': True,
773         }
774
775     def procurement_lines_get(self, cr, uid, ids, *args):
776         res = []
777         for order in self.browse(cr, uid, ids, context={}):
778             for line in order.order_line:
779                 if line.procurement_id:
780                     res.append(line.procurement_id.id)
781         return res
782
783     # if mode == 'finished':
784     #   returns True if all lines are done, False otherwise
785     # if mode == 'canceled':
786     #   returns True if there is at least one canceled line, False otherwise
787     def test_state(self, cr, uid, ids, mode, *args):
788         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
789         finished = True
790         canceled = False
791         notcanceled = False
792         write_done_ids = []
793         write_cancel_ids = []
794         for order in self.browse(cr, uid, ids, context={}):
795             for line in order.order_line:
796                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
797                     if line.state != 'done':
798                         write_done_ids.append(line.id)
799                 else:
800                     finished = False
801                 if line.procurement_id:
802                     if (line.procurement_id.state == 'cancel'):
803                         canceled = True
804                         if line.state != 'exception':
805                             write_cancel_ids.append(line.id)
806                     else:
807                         notcanceled = True
808         if write_done_ids:
809             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
810         if write_cancel_ids:
811             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
812
813         if mode == 'finished':
814             return finished
815         elif mode == 'canceled':
816             return canceled
817             if notcanceled:
818                 return False
819             return canceled
820
821     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
822         return {
823             'name': line.name.split('\n')[0],
824             'origin': order.name,
825             'date_planned': date_planned,
826             'product_id': line.product_id.id,
827             'product_qty': line.product_uom_qty,
828             'product_uom': line.product_uom.id,
829             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
830                     or line.product_uom_qty,
831             'product_uos': (line.product_uos and line.product_uos.id)\
832                     or line.product_uom.id,
833             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
834             'procure_method': line.type,
835             'move_id': move_id,
836             'company_id': order.company_id.id,
837             'note': '\n'.join(line.name.split('\n')[1:]),
838             'property_ids': [(6, 0, [x.id for x in line.property_ids])]
839         }
840
841     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
842         location_id = order.shop_id.warehouse_id.lot_stock_id.id
843         output_id = order.shop_id.warehouse_id.lot_output_id.id
844         return {
845             'name': line.name.split('\n')[0][:250],
846             'picking_id': picking_id,
847             'product_id': line.product_id.id,
848             'date': date_planned,
849             'date_expected': date_planned,
850             'product_qty': line.product_uom_qty,
851             'product_uom': line.product_uom.id,
852             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
853             'product_uos': (line.product_uos and line.product_uos.id)\
854                     or line.product_uom.id,
855             'product_packaging': line.product_packaging.id,
856             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
857             'location_id': location_id,
858             'location_dest_id': output_id,
859             'sale_line_id': line.id,
860             'tracking_id': False,
861             'state': 'draft',
862             #'state': 'waiting',
863             'note': '\n'.join(line.name.split('\n')[1:]),
864             'company_id': order.company_id.id,
865             'price_unit': line.product_id.standard_price or 0.0
866         }
867
868     def _prepare_order_picking(self, cr, uid, order, context=None):
869         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
870         return {
871             'name': pick_name,
872             'origin': order.name,
873             'date': order.date_order,
874             'type': 'out',
875             'state': 'auto',
876             'move_type': order.picking_policy,
877             'sale_id': order.id,
878             'partner_id': order.partner_shipping_id.id,
879             'note': order.note,
880             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
881             'company_id': order.company_id.id,
882         }
883
884     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
885         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
886         """
887         Define ship_recreate for process after shipping exception
888         param order: sale order to which the order lines belong
889         param line: sale order line records to procure
890         param move_id: the ID of stock move
891         param proc_id: the ID of procurement
892         """
893         move_obj = self.pool.get('stock.move')
894         if order.state == 'shipping_except':
895             for pick in order.picking_ids:
896                 for move in pick.move_lines:
897                     if move.state == 'cancel':
898                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
899                         if mov_ids:
900                             for mov in move_obj.browse(cr, uid, mov_ids):
901                                 # 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?
902                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
903                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
904         return True
905
906     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
907         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
908         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
909         return date_planned
910
911     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
912         """Create the required procurements to supply sale order lines, also connecting
913         the procurements to appropriate stock moves in order to bring the goods to the
914         sale order's requested location.
915
916         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
917         a standard outgoing picking will be created to wrap the stock moves, as returned
918         by :meth:`~._prepare_order_picking`.
919
920         Modules that wish to customize the procurements or partition the stock moves over
921         multiple stock pickings may override this method and call ``super()`` with
922         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
923
924         :param browse_record order: sale order to which the order lines belong
925         :param list(browse_record) order_lines: sale order line records to procure
926         :param int picking_id: optional ID of a stock picking to which the created stock moves
927                                will be added. A new picking will be created if ommitted.
928         :return: True
929         """
930         move_obj = self.pool.get('stock.move')
931         picking_obj = self.pool.get('stock.picking')
932         procurement_obj = self.pool.get('procurement.order')
933         proc_ids = []
934
935         for line in order_lines:
936             if line.state == 'done':
937                 continue
938
939             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
940
941             if line.product_id:
942                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
943                     if not picking_id:
944                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
945                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
946                 else:
947                     # a service has no stock move
948                     move_id = False
949
950                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
951                 proc_ids.append(proc_id)
952                 line.write({'procurement_id': proc_id})
953                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
954
955         wf_service = netsvc.LocalService("workflow")
956         if picking_id:
957             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
958             self.delivery_send_note(cr, uid, [order.id], picking_id, context)
959
960
961         for proc_id in proc_ids:
962             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
963
964         val = {}
965         if order.state == 'shipping_except':
966             val['state'] = 'progress'
967             val['shipped'] = False
968
969             if (order.order_policy == 'manual'):
970                 for line in order.order_line:
971                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
972                         val['state'] = 'manual'
973                         break
974         order.write(val)
975         return True
976
977     def action_ship_create(self, cr, uid, ids, context=None):
978         for order in self.browse(cr, uid, ids, context=context):
979             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
980         return True
981
982     def action_ship_end(self, cr, uid, ids, context=None):
983         for order in self.browse(cr, uid, ids, context=context):
984             val = {'shipped': True}
985             if order.state == 'shipping_except':
986                 val['state'] = 'progress'
987                 if (order.order_policy == 'manual'):
988                     for line in order.order_line:
989                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
990                             val['state'] = 'manual'
991                             break
992             for line in order.order_line:
993                 towrite = []
994                 if line.state == 'exception':
995                     towrite.append(line.id)
996                 if towrite:
997                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
998             res = self.write(cr, uid, [order.id], val)
999             if res:
1000                 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1001         return True
1002
1003     def has_stockable_products(self, cr, uid, ids, *args):
1004         for order in self.browse(cr, uid, ids):
1005             for order_line in order.order_line:
1006                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1007                     return True
1008         return False
1009
1010     # ------------------------------------------------
1011     # OpenChatter methods and notifications
1012     # ------------------------------------------------
1013
1014     def get_needaction_user_ids(self, cr, uid, ids, context=None):
1015         result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1016         for obj in self.browse(cr, uid, ids, context=context):
1017             if (obj.state == 'manual' or obj.state == 'progress'):
1018                 result[obj.id].append(obj.user_id.id)
1019         return result
1020
1021     def create_send_note(self, cr, uid, ids, context=None):
1022         for obj in self.browse(cr, uid, ids, context=context):
1023             self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
1024
1025     def confirm_send_note(self, cr, uid, ids, context=None):
1026         for obj in self.browse(cr, uid, ids, context=context):
1027             self.message_append_note(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)
1028
1029     def cancel_send_note(self, cr, uid, ids, context=None):
1030         for obj in self.browse(cr, uid, ids, context=context):
1031             self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1032
1033     def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1034         for order in self.browse(cr, uid, ids, context=context):
1035             for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1036                 # convert datetime field to a datetime, using server format, then
1037                 # convert it to the user TZ and re-render it with %Z to add the timezone
1038                 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1039                 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1040                 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
1041
1042     def delivery_end_send_note(self, cr, uid, ids, context=None):
1043         self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1044
1045     def invoice_paid_send_note(self, cr, uid, ids, context=None):
1046         self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1047
1048     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1049         for order in self.browse(cr, uid, ids, context=context):
1050             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1051                 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
1052
1053     def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1054         return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1055
1056
1057 sale_order()
1058
1059 # TODO add a field price_unit_uos
1060 # - update it on change product and unit price
1061 # - use it in report if there is a uos
1062 class sale_order_line(osv.osv):
1063
1064     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1065         tax_obj = self.pool.get('account.tax')
1066         cur_obj = self.pool.get('res.currency')
1067         res = {}
1068         if context is None:
1069             context = {}
1070         for line in self.browse(cr, uid, ids, context=context):
1071             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1072             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1073             cur = line.order_id.pricelist_id.currency_id
1074             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1075         return res
1076
1077     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1078         res = {}
1079         for line in self.browse(cr, uid, ids, context=context):
1080             try:
1081                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1082             except:
1083                 res[line.id] = 1
1084         return res
1085
1086     def _get_uom_id(self, cr, uid, *args):
1087         try:
1088             proxy = self.pool.get('ir.model.data')
1089             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1090             return result[1]
1091         except Exception, ex:
1092             return False
1093
1094     _name = 'sale.order.line'
1095     _description = 'Sales Order Line'
1096     _columns = {
1097         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1098         'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1099         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1100         '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)]}),
1101         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1102         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1103         'invoiced': fields.boolean('Invoiced', readonly=True),
1104         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1105         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
1106         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
1107         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1108         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1109             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."),
1110         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1111         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1112         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1113         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1114         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1115         'product_uos': fields.many2one('product.uom', 'Product UoS'),
1116         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1117         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1118         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
1119         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1120         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1121         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1122                 help='* The \'Draft\' state is set when the related sales order in draft state. \
1123                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1124                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
1125                     \n* The \'Done\' state is set when the sales order line has been picked. \
1126                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1127         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1128         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1129         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1130     }
1131     _order = 'sequence, id'
1132     _defaults = {
1133         'product_uom' : _get_uom_id,
1134         'discount': 0.0,
1135         'delay': 0.0,
1136         'product_uom_qty': 1,
1137         'product_uos_qty': 1,
1138         'sequence': 10,
1139         'invoiced': 0,
1140         'state': 'draft',
1141         'type': 'make_to_stock',
1142         'product_packaging': False,
1143         'price_unit': 0.0,
1144     }
1145
1146     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1147         """Prepare the dict of values to create the new invoice line for a
1148            sale order line. This method may be overridden to implement custom
1149            invoice generation (making sure to call super() to establish
1150            a clean extension chain).
1151
1152            :param browse_record line: sale.order.line record to invoice
1153            :param int account_id: optional ID of a G/L account to force
1154                (this is used for returning products including service)
1155            :return: dict of values to create() the invoice line
1156         """
1157
1158         def _get_line_qty(line):
1159             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1160                 if line.product_uos:
1161                     return line.product_uos_qty or 0.0
1162                 return line.product_uom_qty
1163             else:
1164                 return self.pool.get('procurement.order').quantity_get(cr, uid,
1165                         line.procurement_id.id, context=context)
1166
1167         def _get_line_uom(line):
1168             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1169                 if line.product_uos:
1170                     return line.product_uos.id
1171                 return line.product_uom.id
1172             else:
1173                 return self.pool.get('procurement.order').uom_get(cr, uid,
1174                         line.procurement_id.id, context=context)
1175
1176         if not line.invoiced:
1177             if not account_id:
1178                 if line.product_id:
1179                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1180                     if not account_id:
1181                         account_id = line.product_id.categ_id.property_account_income_categ.id
1182                     if not account_id:
1183                         raise osv.except_osv(_('Error!'),
1184                                 _('Please define income account for this product: "%s" (id:%d).') % \
1185                                     (line.product_id.name, line.product_id.id,))
1186                 else:
1187                     prop = self.pool.get('ir.property').get(cr, uid,
1188                             'property_account_income_categ', 'product.category',
1189                             context=context)
1190                     account_id = prop and prop.id or False
1191             uosqty = _get_line_qty(line)
1192             uos_id = _get_line_uom(line)
1193             pu = 0.0
1194             if uosqty:
1195                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1196                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
1197             fpos = line.order_id.fiscal_position or False
1198             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1199             if not account_id:
1200                 raise osv.except_osv(_('Error!'),
1201                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
1202             return {
1203                 'name': line.name,
1204                 'origin': line.order_id.name,
1205                 'account_id': account_id,
1206                 'price_unit': pu,
1207                 'quantity': uosqty,
1208                 'discount': line.discount,
1209                 'uos_id': uos_id,
1210                 'product_id': line.product_id.id or False,
1211                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1212                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1213             }
1214
1215         return False
1216
1217     def invoice_line_create(self, cr, uid, ids, context=None):
1218         if context is None:
1219             context = {}
1220
1221         create_ids = []
1222         sales = set()
1223         for line in self.browse(cr, uid, ids, context=context):
1224             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1225             if vals:
1226                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1227                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1228                 self.write(cr, uid, [line.id], {'invoiced': True})
1229                 sales.add(line.order_id.id)
1230                 create_ids.append(inv_id)
1231         # Trigger workflow events
1232         wf_service = netsvc.LocalService("workflow")
1233         for sale_id in sales:
1234             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1235         return create_ids
1236
1237     def button_cancel(self, cr, uid, ids, context=None):
1238         for line in self.browse(cr, uid, ids, context=context):
1239             if line.invoiced:
1240                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
1241             for move_line in line.move_ids:
1242                 if move_line.state != 'cancel':
1243                     raise osv.except_osv(
1244                             _('Cannot cancel sales order line!'),
1245                             _('You must first cancel stock moves attached to this sales order line.'))
1246         return self.write(cr, uid, ids, {'state': 'cancel'})
1247
1248     def button_confirm(self, cr, uid, ids, context=None):
1249         return self.write(cr, uid, ids, {'state': 'confirmed'})
1250
1251     def button_done(self, cr, uid, ids, context=None):
1252         wf_service = netsvc.LocalService("workflow")
1253         res = self.write(cr, uid, ids, {'state': 'done'})
1254         for line in self.browse(cr, uid, ids, context=context):
1255             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1256         return res
1257
1258     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1259         product_obj = self.pool.get('product.product')
1260         if not product_id:
1261             return {'value': {'product_uom': product_uos,
1262                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1263
1264         product = product_obj.browse(cr, uid, product_id)
1265         value = {
1266             'product_uom': product.uom_id.id,
1267         }
1268         # FIXME must depend on uos/uom of the product and not only of the coeff.
1269         try:
1270             value.update({
1271                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1272                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1273             })
1274         except ZeroDivisionError:
1275             pass
1276         return {'value': value}
1277
1278     def copy_data(self, cr, uid, id, default=None, context=None):
1279         if not default:
1280             default = {}
1281         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1282         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1283
1284     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1285                                    partner_id=False, packaging=False, flag=False, context=None):
1286         if not product:
1287             return {'value': {'product_packaging': False}}
1288         product_obj = self.pool.get('product.product')
1289         product_uom_obj = self.pool.get('product.uom')
1290         pack_obj = self.pool.get('product.packaging')
1291         warning = {}
1292         result = {}
1293         warning_msgs = ''
1294         if flag:
1295             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1296                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1297                     packaging=packaging, flag=False, context=context)
1298             warning_msgs = res.get('warning') and res['warning']['message']
1299
1300         products = product_obj.browse(cr, uid, product, context=context)
1301         if not products.packaging:
1302             packaging = result['product_packaging'] = False
1303         elif not packaging and products.packaging and not flag:
1304             packaging = products.packaging[0].id
1305             result['product_packaging'] = packaging
1306
1307         if packaging:
1308             default_uom = products.uom_id and products.uom_id.id
1309             pack = pack_obj.browse(cr, uid, packaging, context=context)
1310             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1311 #            qty = qty - qty % q + q
1312             if qty and (q and not (qty % q) == 0):
1313                 ean = pack.ean or _('(n/a)')
1314                 qty_pack = pack.qty
1315                 type_ul = pack.ul
1316                 if not warning_msgs:
1317                     warn_msg = _("You selected a quantity of %d Units.\n"
1318                                 "But it's not compatible with the selected packaging.\n"
1319                                 "Here is a proposition of quantities according to the packaging:\n"
1320                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1321                                     (qty, ean, qty_pack, type_ul.name)
1322                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1323                 warning = {
1324                        'title': _('Configuration Error!'),
1325                        'message': warning_msgs
1326                 }
1327             result['product_uom_qty'] = qty
1328
1329         return {'value': result, 'warning': warning}
1330
1331     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1332             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1333             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1334         context = context or {}
1335         lang = lang or context.get('lang',False)
1336         if not  partner_id:
1337             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1338         warning = {}
1339         product_uom_obj = self.pool.get('product.uom')
1340         partner_obj = self.pool.get('res.partner')
1341         product_obj = self.pool.get('product.product')
1342         context = {'lang': lang, 'partner_id': partner_id}
1343         if partner_id:
1344             lang = partner_obj.browse(cr, uid, partner_id).lang
1345         context_partner = {'lang': lang, 'partner_id': partner_id}
1346
1347         if not product:
1348             return {'value': {'th_weight': 0, 'product_packaging': False,
1349                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1350                    'product_uos': []}}
1351         if not date_order:
1352             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1353
1354         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1355         result = res.get('value', {})
1356         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1357         product_obj = product_obj.browse(cr, uid, product, context=context)
1358
1359         uom2 = False
1360         if uom:
1361             uom2 = product_uom_obj.browse(cr, uid, uom)
1362             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1363                 uom = False
1364         if uos:
1365             if product_obj.uos_id:
1366                 uos2 = product_uom_obj.browse(cr, uid, uos)
1367                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1368                     uos = False
1369             else:
1370                 uos = False
1371         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1372         if update_tax: #The quantity only have changed
1373             result['delay'] = (product_obj.sale_delay or 0.0)
1374             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1375             result.update({'type': product_obj.procure_method})
1376
1377         if not flag:
1378             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1379             if product_obj.description_sale:
1380                 result['name'] += '\n'+product_obj.description_sale
1381         domain = {}
1382         if (not uom) and (not uos):
1383             result['product_uom'] = product_obj.uom_id.id
1384             if product_obj.uos_id:
1385                 result['product_uos'] = product_obj.uos_id.id
1386                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1387                 uos_category_id = product_obj.uos_id.category_id.id
1388             else:
1389                 result['product_uos'] = False
1390                 result['product_uos_qty'] = qty
1391                 uos_category_id = False
1392             result['th_weight'] = qty * product_obj.weight
1393             domain = {'product_uom':
1394                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1395                         'product_uos':
1396                         [('category_id', '=', uos_category_id)]}
1397         elif uos and not uom: # only happens if uom is False
1398             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1399             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1400             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1401         elif uom: # whether uos is set or not
1402             default_uom = product_obj.uom_id and product_obj.uom_id.id
1403             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1404             if product_obj.uos_id:
1405                 result['product_uos'] = product_obj.uos_id.id
1406                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1407             else:
1408                 result['product_uos'] = False
1409                 result['product_uos_qty'] = qty
1410             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1411
1412         if not uom2:
1413             uom2 = product_obj.uom_id
1414         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1415         if (product_obj.type=='product') and int(compare_qty) == -1 \
1416           and (product_obj.procure_method=='make_to_stock'):
1417             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1418                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1419                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1420                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1421             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1422         # get unit price
1423
1424         if not pricelist:
1425             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1426                     'Please set one before choosing a product.')
1427             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1428         else:
1429             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1430                     product, qty or 1.0, partner_id, {
1431                         'uom': uom or result.get('product_uom'),
1432                         'date': date_order,
1433                         })[pricelist]
1434             if price is False:
1435                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1436                         "You have to change either the product, the quantity or the pricelist.")
1437
1438                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1439             else:
1440                 result.update({'price_unit': price})
1441         if warning_msgs:
1442             warning = {
1443                        'title': _('Configuration Error!'),
1444                        'message' : warning_msgs
1445                     }
1446         return {'value': result, 'domain': domain, 'warning': warning}
1447
1448     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1449             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1450             lang=False, update_tax=True, date_order=False, context=None):
1451         context = context or {}
1452         lang = lang or ('lang' in context and context['lang'])
1453         if not uom:
1454             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1455         return self.product_id_change(cursor, user, ids, pricelist, product,
1456                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1457                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1458                 date_order=date_order, context=context)
1459
1460     def unlink(self, cr, uid, ids, context=None):
1461         if context is None:
1462             context = {}
1463         """Allows to delete sales order lines in draft,cancel states"""
1464         for rec in self.browse(cr, uid, ids, context=context):
1465             if rec.state not in ['draft', 'cancel']:
1466                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1467         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1468
1469 sale_order_line()
1470
1471 class mail_message(osv.osv):
1472     _inherit = 'mail.message'
1473
1474     def _postprocess_sent_message(self, cr, uid, message, context=None):
1475         if message.model == 'sale.order':
1476             wf_service = netsvc.LocalService("workflow")
1477             wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1478         return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1479
1480 mail_message()
1481
1482 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: