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