[IMP]Sale : Warinig Messages are Changed
[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('Sale Price'), 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('Sale Price'), 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('Sale Price'), 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 before ! To cancel a sale order, 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                 _('There is no sales journal defined 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         mod_obj = self.pool.get('ir.model.data')
487         wf_service = netsvc.LocalService("workflow")
488         inv_ids = set()
489         inv_ids1 = set()
490         for id in ids:
491             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
492                 inv_ids.add(record.id)
493         # inv_ids would have old invoices if any
494         for id in ids:
495             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
496             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
497                 inv_ids1.add(record.id)
498         inv_ids = list(inv_ids1.difference(inv_ids))
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': inv_ids and 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                         _('Could not cancel sales order!'),
706                         _('First cancelled all picking attached to this sale 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                         _('Could not cancel this sales order!'),
720                         _('First cancelled all invoices attached to this sale 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,
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': line.notes
849         }
850
851     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
852         location_id = order.shop_id.warehouse_id.lot_stock_id.id
853         output_id = order.shop_id.warehouse_id.lot_output_id.id
854         return {
855             'name': line.name[:250],
856             'picking_id': picking_id,
857             'product_id': line.product_id.id,
858             'date': date_planned,
859             'date_expected': date_planned,
860             'product_qty': line.product_uom_qty,
861             'product_uom': line.product_uom.id,
862             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
863             'product_uos': (line.product_uos and line.product_uos.id)\
864                     or line.product_uom.id,
865             'product_packaging': line.product_packaging.id,
866             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
867             'location_id': location_id,
868             'location_dest_id': output_id,
869             'sale_line_id': line.id,
870             'tracking_id': False,
871             'state': 'draft',
872             #'state': 'waiting',
873             'note': line.notes,
874             'company_id': order.company_id.id,
875             'price_unit': line.product_id.standard_price or 0.0
876         }
877
878     def _prepare_order_picking(self, cr, uid, order, context=None):
879         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
880         return {
881             'name': pick_name,
882             'origin': order.name,
883             'date': order.date_order,
884             'type': 'out',
885             'state': 'auto',
886             'move_type': order.picking_policy,
887             'sale_id': order.id,
888             'partner_id': order.partner_shipping_id.id,
889             'note': order.note,
890             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
891             'company_id': order.company_id.id,
892         }
893
894     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
895         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
896         """
897         Define ship_recreate for process after shipping exception
898         param order: sale order to which the order lines belong
899         param line: sale order line records to procure
900         param move_id: the ID of stock move
901         param proc_id: the ID of procurement
902         """
903         move_obj = self.pool.get('stock.move')
904         if order.state == 'shipping_except':
905             for pick in order.picking_ids:
906                 for move in pick.move_lines:
907                     if move.state == 'cancel':
908                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
909                         if mov_ids:
910                             for mov in move_obj.browse(cr, uid, mov_ids):
911                                 # 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?
912                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
913                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
914         return True
915
916     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
917         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
918         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
919         return date_planned
920
921     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
922         """Create the required procurements to supply sale order lines, also connecting
923         the procurements to appropriate stock moves in order to bring the goods to the
924         sale order's requested location.
925
926         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
927         a standard outgoing picking will be created to wrap the stock moves, as returned
928         by :meth:`~._prepare_order_picking`.
929
930         Modules that wish to customize the procurements or partition the stock moves over
931         multiple stock pickings may override this method and call ``super()`` with
932         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
933
934         :param browse_record order: sale order to which the order lines belong
935         :param list(browse_record) order_lines: sale order line records to procure
936         :param int picking_id: optional ID of a stock picking to which the created stock moves
937                                will be added. A new picking will be created if ommitted.
938         :return: True
939         """
940         move_obj = self.pool.get('stock.move')
941         picking_obj = self.pool.get('stock.picking')
942         procurement_obj = self.pool.get('procurement.order')
943         proc_ids = []
944
945         for line in order_lines:
946             if line.state == 'done':
947                 continue
948
949             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
950
951             if line.product_id:
952                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
953                     if not picking_id:
954                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
955                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
956                 else:
957                     # a service has no stock move
958                     move_id = False
959
960                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
961                 proc_ids.append(proc_id)
962                 line.write({'procurement_id': proc_id})
963                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
964
965         wf_service = netsvc.LocalService("workflow")
966         if picking_id:
967             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
968             self.delivery_send_note(cr, uid, [order.id], picking_id, context)
969
970
971         for proc_id in proc_ids:
972             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
973
974         val = {}
975         if order.state == 'shipping_except':
976             val['state'] = 'progress'
977             val['shipped'] = False
978
979             if (order.order_policy == 'manual'):
980                 for line in order.order_line:
981                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
982                         val['state'] = 'manual'
983                         break
984         order.write(val)
985         return True
986
987     def action_ship_create(self, cr, uid, ids, context=None):
988         for order in self.browse(cr, uid, ids, context=context):
989             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
990         return True
991
992     def action_ship_end(self, cr, uid, ids, context=None):
993         for order in self.browse(cr, uid, ids, context=context):
994             val = {'shipped': True}
995             if order.state == 'shipping_except':
996                 val['state'] = 'progress'
997                 if (order.order_policy == 'manual'):
998                     for line in order.order_line:
999                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
1000                             val['state'] = 'manual'
1001                             break
1002             for line in order.order_line:
1003                 towrite = []
1004                 if line.state == 'exception':
1005                     towrite.append(line.id)
1006                 if towrite:
1007                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
1008             res = self.write(cr, uid, [order.id], val)
1009             if res:
1010                 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1011         return True
1012
1013     def has_stockable_products(self, cr, uid, ids, *args):
1014         for order in self.browse(cr, uid, ids):
1015             for order_line in order.order_line:
1016                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1017                     return True
1018         return False
1019     
1020     # ------------------------------------------------
1021     # OpenChatter methods and notifications
1022     # ------------------------------------------------
1023     
1024     def get_needaction_user_ids(self, cr, uid, ids, context=None):
1025         result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1026         for obj in self.browse(cr, uid, ids, context=context):
1027             if (obj.state == 'manual' or obj.state == 'progress'):
1028                 result[obj.id].append(obj.user_id.id)
1029         return result
1030  
1031     def create_send_note(self, cr, uid, ids, context=None):
1032         for obj in self.browse(cr, uid, ids, context=context):
1033             self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
1034             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)
1035         
1036     def confirm_send_note(self, cr, uid, ids, context=None):
1037         for obj in self.browse(cr, uid, ids, context=context):
1038             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)
1039     
1040     def cancel_send_note(self, cr, uid, ids, context=None):
1041         for obj in self.browse(cr, uid, ids, context=context):
1042             self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1043         
1044     def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1045         for order in self.browse(cr, uid, ids, context=context):
1046             for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1047                 # convert datetime field to a datetime, using server format, then
1048                 # convert it to the user TZ and re-render it with %Z to add the timezone
1049                 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1050                 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1051                 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)
1052     
1053     def delivery_end_send_note(self, cr, uid, ids, context=None):
1054         self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1055      
1056     def invoice_paid_send_note(self, cr, uid, ids, context=None):
1057         self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1058         
1059     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1060         for order in self.browse(cr, uid, ids, context=context):
1061             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1062                 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)
1063     
1064     def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1065         return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1066             
1067         
1068 sale_order()
1069
1070 # TODO add a field price_unit_uos
1071 # - update it on change product and unit price
1072 # - use it in report if there is a uos
1073 class sale_order_line(osv.osv):
1074
1075     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1076         tax_obj = self.pool.get('account.tax')
1077         cur_obj = self.pool.get('res.currency')
1078         res = {}
1079         if context is None:
1080             context = {}
1081         for line in self.browse(cr, uid, ids, context=context):
1082             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1083             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1084             cur = line.order_id.pricelist_id.currency_id
1085             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1086         return res
1087
1088     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1089         res = {}
1090         for line in self.browse(cr, uid, ids, context=context):
1091             try:
1092                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1093             except:
1094                 res[line.id] = 1
1095         return res
1096
1097     def _get_uom_id(self, cr, uid, *args):
1098         try:
1099             proxy = self.pool.get('ir.model.data')
1100             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1101             return result[1]
1102         except Exception, ex:
1103             return False
1104
1105     _name = 'sale.order.line'
1106     _description = 'Sales Order Line'
1107     _columns = {
1108         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1109         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1110         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1111         '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)]}),
1112         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1113         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1114         'invoiced': fields.boolean('Invoiced', readonly=True),
1115         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1116         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1117         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1118         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1119         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1120             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."),
1121         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1122         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1123         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1124         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1125         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1126         'product_uos': fields.many2one('product.uom', 'Product UoS'),
1127         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1128         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1129         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1130         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1131         'notes': fields.text('Notes'),
1132         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1133         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1134                 help='* The \'Draft\' state is set when the related sales order in draft state. \
1135                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1136                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
1137                     \n* The \'Done\' state is set when the sales order line has been picked. \
1138                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1139         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1140         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1141         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1142     }
1143     _order = 'sequence, id'
1144     _defaults = {
1145         'product_uom' : _get_uom_id,
1146         'discount': 0.0,
1147         'delay': 0.0,
1148         'product_uom_qty': 1,
1149         'product_uos_qty': 1,
1150         'sequence': 10,
1151         'invoiced': 0,
1152         'state': 'draft',
1153         'type': 'make_to_stock',
1154         'product_packaging': False,
1155         'price_unit': 0.0,
1156     }
1157
1158     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1159         """Prepare the dict of values to create the new invoice line for a
1160            sale order line. This method may be overridden to implement custom
1161            invoice generation (making sure to call super() to establish
1162            a clean extension chain).
1163
1164            :param browse_record line: sale.order.line record to invoice
1165            :param int account_id: optional ID of a G/L account to force
1166                (this is used for returning products including service)
1167            :return: dict of values to create() the invoice line
1168         """
1169
1170         def _get_line_qty(line):
1171             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1172                 if line.product_uos:
1173                     return line.product_uos_qty or 0.0
1174                 return line.product_uom_qty
1175             else:
1176                 return self.pool.get('procurement.order').quantity_get(cr, uid,
1177                         line.procurement_id.id, context=context)
1178
1179         def _get_line_uom(line):
1180             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1181                 if line.product_uos:
1182                     return line.product_uos.id
1183                 return line.product_uom.id
1184             else:
1185                 return self.pool.get('procurement.order').uom_get(cr, uid,
1186                         line.procurement_id.id, context=context)
1187
1188         if not line.invoiced:
1189             if not account_id:
1190                 if line.product_id:
1191                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1192                     if not account_id:
1193                         account_id = line.product_id.categ_id.property_account_income_categ.id
1194                     if not account_id:
1195                         raise osv.except_osv(_('Error !'),
1196                                 _('There is no income account defined for this product: "%s" (id:%d)') % \
1197                                     (line.product_id.name, line.product_id.id,))
1198                 else:
1199                     prop = self.pool.get('ir.property').get(cr, uid,
1200                             'property_account_income_categ', 'product.category',
1201                             context=context)
1202                     account_id = prop and prop.id or False
1203             uosqty = _get_line_qty(line)
1204             uos_id = _get_line_uom(line)
1205             pu = 0.0
1206             if uosqty:
1207                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1208                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1209             fpos = line.order_id.fiscal_position or False
1210             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1211             if not account_id:
1212                 raise osv.except_osv(_('Error !'),
1213                             _('There is no Fiscal Position defined or income category account defined for Product Categories default Properties.'))
1214             return {
1215                 'name': line.name,
1216                 'origin': line.order_id.name,
1217                 'account_id': account_id,
1218                 'price_unit': pu,
1219                 'quantity': uosqty,
1220                 'discount': line.discount,
1221                 'uos_id': uos_id,
1222                 'product_id': line.product_id.id or False,
1223                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1224                 'note': line.notes,
1225                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1226             }
1227
1228         return False
1229
1230     def invoice_line_create(self, cr, uid, ids, context=None):
1231         if context is None:
1232             context = {}
1233
1234         create_ids = []
1235         sales = set()
1236         for line in self.browse(cr, uid, ids, context=context):
1237             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1238             if vals:
1239                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1240                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1241                 self.write(cr, uid, [line.id], {'invoiced': True})
1242                 sales.add(line.order_id.id)
1243                 create_ids.append(inv_id)
1244         # Trigger workflow events
1245         wf_service = netsvc.LocalService("workflow")
1246         for sale_id in sales:
1247             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1248         return create_ids
1249
1250     def button_cancel(self, cr, uid, ids, context=None):
1251         for line in self.browse(cr, uid, ids, context=context):
1252             if line.invoiced:
1253                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1254             for move_line in line.move_ids:
1255                 if move_line.state != 'cancel':
1256                     raise osv.except_osv(
1257                             _('Cannot cancel sale order line!'),
1258                             _('You must first cancel stock moves attached to this sales order line.'))
1259         return self.write(cr, uid, ids, {'state': 'cancel'})
1260
1261     def button_confirm(self, cr, uid, ids, context=None):
1262         return self.write(cr, uid, ids, {'state': 'confirmed'})
1263
1264     def button_done(self, cr, uid, ids, context=None):
1265         wf_service = netsvc.LocalService("workflow")
1266         res = self.write(cr, uid, ids, {'state': 'done'})
1267         for line in self.browse(cr, uid, ids, context=context):
1268             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1269         return res
1270
1271     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1272         product_obj = self.pool.get('product.product')
1273         if not product_id:
1274             return {'value': {'product_uom': product_uos,
1275                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1276
1277         product = product_obj.browse(cr, uid, product_id)
1278         value = {
1279             'product_uom': product.uom_id.id,
1280         }
1281         # FIXME must depend on uos/uom of the product and not only of the coeff.
1282         try:
1283             value.update({
1284                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1285                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1286             })
1287         except ZeroDivisionError:
1288             pass
1289         return {'value': value}
1290
1291     def copy_data(self, cr, uid, id, default=None, context=None):
1292         if not default:
1293             default = {}
1294         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1295         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1296
1297     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1298                                    partner_id=False, packaging=False, flag=False, context=None):
1299         if not product:
1300             return {'value': {'product_packaging': False}}
1301         product_obj = self.pool.get('product.product')
1302         product_uom_obj = self.pool.get('product.uom')
1303         pack_obj = self.pool.get('product.packaging')
1304         warning = {}
1305         result = {}
1306         warning_msgs = ''
1307         if flag:
1308             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1309                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1310                     packaging=packaging, flag=False, context=context)
1311             warning_msgs = res.get('warning') and res['warning']['message']
1312
1313         products = product_obj.browse(cr, uid, product, context=context)
1314         if not products.packaging:
1315             packaging = result['product_packaging'] = False
1316         elif not packaging and products.packaging and not flag:
1317             packaging = products.packaging[0].id
1318             result['product_packaging'] = packaging
1319
1320         if packaging:
1321             default_uom = products.uom_id and products.uom_id.id
1322             pack = pack_obj.browse(cr, uid, packaging, context=context)
1323             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1324 #            qty = qty - qty % q + q
1325             if qty and (q and not (qty % q) == 0):
1326                 ean = pack.ean or _('(n/a)')
1327                 qty_pack = pack.qty
1328                 type_ul = pack.ul
1329                 if not warning_msgs:
1330                     warn_msg = _("You selected a quantity of %d Units.\n"
1331                                 "But it's not compatible with the selected packaging.\n"
1332                                 "Here is a proposition of quantities according to the packaging:\n"
1333                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1334                                     (qty, ean, qty_pack, type_ul.name)
1335                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1336                 warning = {
1337                        'title': _('Configuration Error !'),
1338                        'message': warning_msgs
1339                 }
1340             result['product_uom_qty'] = qty
1341
1342         return {'value': result, 'warning': warning}
1343
1344     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1345             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1346             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1347         context = context or {}
1348         lang = lang or context.get('lang',False)
1349         if not  partner_id:
1350             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1351         warning = {}
1352         product_uom_obj = self.pool.get('product.uom')
1353         partner_obj = self.pool.get('res.partner')
1354         product_obj = self.pool.get('product.product')
1355         context = {'lang': lang, 'partner_id': partner_id}
1356         if partner_id:
1357             lang = partner_obj.browse(cr, uid, partner_id).lang
1358         context_partner = {'lang': lang, 'partner_id': partner_id}
1359
1360         if not product:
1361             return {'value': {'th_weight': 0, 'product_packaging': False,
1362                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1363                    'product_uos': []}}
1364         if not date_order:
1365             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1366
1367         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1368         result = res.get('value', {})
1369         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1370         product_obj = product_obj.browse(cr, uid, product, context=context)
1371
1372         uom2 = False
1373         if uom:
1374             uom2 = product_uom_obj.browse(cr, uid, uom)
1375             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1376                 uom = False
1377         if uos:
1378             if product_obj.uos_id:
1379                 uos2 = product_uom_obj.browse(cr, uid, uos)
1380                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1381                     uos = False
1382             else:
1383                 uos = False
1384         if product_obj.description_sale:
1385             result['notes'] = product_obj.description_sale
1386         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1387         if update_tax: #The quantity only have changed
1388             result['delay'] = (product_obj.sale_delay or 0.0)
1389             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1390             result.update({'type': product_obj.procure_method})
1391
1392         if not flag:
1393             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1394         domain = {}
1395         if (not uom) and (not uos):
1396             result['product_uom'] = product_obj.uom_id.id
1397             if product_obj.uos_id:
1398                 result['product_uos'] = product_obj.uos_id.id
1399                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1400                 uos_category_id = product_obj.uos_id.category_id.id
1401             else:
1402                 result['product_uos'] = False
1403                 result['product_uos_qty'] = qty
1404                 uos_category_id = False
1405             result['th_weight'] = qty * product_obj.weight
1406             domain = {'product_uom':
1407                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1408                         'product_uos':
1409                         [('category_id', '=', uos_category_id)]}
1410
1411         elif uos and not uom: # only happens if uom is False
1412             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1413             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1414             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1415         elif uom: # whether uos is set or not
1416             default_uom = product_obj.uom_id and product_obj.uom_id.id
1417             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1418             if product_obj.uos_id:
1419                 result['product_uos'] = product_obj.uos_id.id
1420                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1421             else:
1422                 result['product_uos'] = False
1423                 result['product_uos_qty'] = qty
1424             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1425
1426         if not uom2:
1427             uom2 = product_obj.uom_id
1428         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1429         if (product_obj.type=='product') and int(compare_qty) == -1 \
1430           and (product_obj.procure_method=='make_to_stock'):
1431             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1432                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1433                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1434                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1435             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1436         # get unit price
1437
1438         if not pricelist:
1439             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1440                     'Please set one before choosing a product.')
1441             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1442         else:
1443             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1444                     product, qty or 1.0, partner_id, {
1445                         'uom': uom or result.get('product_uom'),
1446                         'date': date_order,
1447                         })[pricelist]
1448             if price is False:
1449                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1450                         "You have to change either the product, the quantity or the pricelist.")
1451
1452                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1453             else:
1454                 result.update({'price_unit': price})
1455         if warning_msgs:
1456             warning = {
1457                        'title': _('Configuration Error !'),
1458                        'message' : warning_msgs
1459                     }
1460         return {'value': result, 'domain': domain, 'warning': warning}
1461
1462     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1463             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1464             lang=False, update_tax=True, date_order=False, context=None):
1465         context = context or {}
1466         lang = lang or ('lang' in context and context['lang'])
1467         res = self.product_id_change(cursor, user, ids, pricelist, product,
1468                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1469                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1470                 date_order=date_order, context=context)
1471         if 'product_uom' in res['value']:
1472             del res['value']['product_uom']
1473         if not uom:
1474             res['value']['price_unit'] = 0.0
1475         return res
1476
1477     def unlink(self, cr, uid, ids, context=None):
1478         if context is None:
1479             context = {}
1480         """Allows to delete sales order lines in draft,cancel states"""
1481         for rec in self.browse(cr, uid, ids, context=context):
1482             if rec.state not in ['draft', 'cancel']:
1483                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1484         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1485
1486 sale_order_line()
1487
1488 class mail_message(osv.osv):
1489     _inherit = 'mail.message'
1490     
1491     def _postprocess_sent_message(self, cr, uid, message, context=None):
1492         if message.model == 'sale.order':
1493             wf_service = netsvc.LocalService("workflow")
1494             wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr) 
1495         return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1496
1497 mail_message()
1498
1499 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: