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