[MERGE]:trunk
[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         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40         'company_id': fields.many2one('res.company', 'Company', required=False),
41     }
42     _defaults = {
43         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
44     }
45
46 sale_shop()
47
48 class sale_order(osv.osv):
49     _name = "sale.order"
50     _inherit = ['mail.thread', 'ir.needaction_mixin']
51     _description = "Sales Order"
52
53     def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
54         v = {}
55         if shop_id:
56             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
57             if shop.project_id.id:
58                 v['project_id'] = shop.project_id.id
59             if shop.pricelist_id.id:
60                 v['pricelist_id'] = shop.pricelist_id.id
61         return {'value': v}
62
63     def copy(self, cr, uid, id, default=None, context=None):
64         if not default:
65             default = {}
66         default.update({
67             'state': 'draft',
68             'invoice_ids': [],
69             'date_confirm': False,
70             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
71         })
72         return super(sale_order, self).copy(cr, uid, id, default, context=context)
73
74     def _amount_line_tax(self, cr, uid, line, context=None):
75         val = 0.0
76         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']:
77             val += c.get('amount', 0.0)
78         return val
79
80     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
81         cur_obj = self.pool.get('res.currency')
82         res = {}
83         for order in self.browse(cr, uid, ids, context=context):
84             res[order.id] = {
85                 'amount_untaxed': 0.0,
86                 'amount_tax': 0.0,
87                 'amount_total': 0.0,
88             }
89             val = val1 = 0.0
90             cur = order.pricelist_id.currency_id
91             for line in order.order_line:
92                 val1 += line.price_subtotal
93                 val += self._amount_line_tax(cr, uid, line, context=context)
94             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
95             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
96             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
97         return res
98
99
100     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
101         res = {}
102         for sale in self.browse(cursor, user, ids, context=context):
103             if sale.invoiced:
104                 res[sale.id] = 100.0
105                 continue
106             tot = 0.0
107             for invoice in sale.invoice_ids:
108                 if invoice.state not in ('draft', 'cancel'):
109                     tot += invoice.amount_untaxed
110             if tot:
111                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
112             else:
113                 res[sale.id] = 0.0
114         return res
115
116     def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
117         res = {}
118         for sale in self.browse(cursor, user, ids, context=context):
119             res[sale.id] = False
120             if sale.invoice_ids:
121                 res[sale.id] = True
122         return res
123
124     def _invoiced(self, cursor, user, ids, name, arg, context=None):
125         res = {}
126         for sale in self.browse(cursor, user, ids, context=context):
127             res[sale.id] = True
128             invoice_existence = False
129             for invoice in sale.invoice_ids:
130                 if invoice.state!='cancel':
131                     invoice_existence = True
132                     if invoice.state != 'paid':
133                         res[sale.id] = False
134                         break
135             if not invoice_existence or sale.state == 'manual':
136                 res[sale.id] = False
137         return res
138
139     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
140         if not len(args):
141             return []
142         clause = ''
143         sale_clause = ''
144         no_invoiced = False
145         for arg in args:
146             if arg[1] == '=':
147                 if arg[2]:
148                     clause += 'AND inv.state = \'paid\''
149                 else:
150                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
151                     sale_clause = ',  sale_order AS sale '
152                     no_invoiced = True
153
154         cursor.execute('SELECT rel.order_id ' \
155                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
156                 'WHERE rel.invoice_id = inv.id ' + clause)
157         res = cursor.fetchall()
158         if no_invoiced:
159             cursor.execute('SELECT sale.id ' \
160                     'FROM sale_order AS sale ' \
161                     'WHERE sale.id NOT IN ' \
162                         '(SELECT rel.order_id ' \
163                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
164             res.extend(cursor.fetchall())
165         if not res:
166             return [('id', '=', 0)]
167         return [('id', 'in', [x[0] for x in res])]
168
169     def _get_order(self, cr, uid, ids, context=None):
170         result = {}
171         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
172             result[line.order_id.id] = True
173         return result.keys()
174
175     _columns = {
176         'name': fields.char('Order Reference', size=64, required=True,
177             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
178         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
180         'client_order_ref': fields.char('Customer Reference', size=64),
181         'state': fields.selection([
182             ('draft', 'Draft Quotation'),
183             ('sent', 'Quotation Sent'),
184             ('cancel', 'Cancelled'),
185             ('waiting_date', 'Waiting Schedule'),
186             ('progress', 'Sale Order'),
187             ('manual', 'Sale to Invoice'),
188             ('invoice_except', 'Invoice Exception'),
189             ('done', 'Done'),
190             ], 'Status', readonly=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
191         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
193         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
194         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
195         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
196         '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."),
197         'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
198         'order_policy': fields.selection([
199                 ('manual', 'On Demand'),
200             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
201             help="""This field controls how invoice and delivery operations are synchronized.
202   - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
203         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
204         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", readonly=True, required=True),
205         'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
206
207         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
208         '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)."),
209         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
210         'invoiced': fields.function(_invoiced, string='Paid',
211             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
212         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
213             fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."),
214         'note': fields.text('Terms and conditions'),
215
216         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
217             store = {
218                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
219                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
220             },
221             multi='sums', help="The amount without tax."),
222         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
223             store = {
224                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
226             },
227             multi='sums', help="The tax amount."),
228         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
229             store = {
230                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
231                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
232             },
233             multi='sums', help="The total amount."),
234
235         'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
236         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
237         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
238         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
239     }
240     _defaults = {
241         'date_order': fields.date.context_today,
242         'order_policy': 'manual',
243         'state': 'draft',
244         'user_id': lambda obj, cr, uid, context: uid,
245         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
246         'invoice_quantity': 'order',
247         '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'],
248         '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'],
249     }
250     _sql_constraints = [
251         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
252     ]
253     _order = 'name desc'
254
255     # Form filling
256     def unlink(self, cr, uid, ids, context=None):
257         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
258         unlink_ids = []
259         for s in sale_orders:
260             if s['state'] in ['draft', 'cancel']:
261                 unlink_ids.append(s['id'])
262             else:
263                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
264
265         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
266
267     def copy_quotation(self, cr, uid, ids, context=None):
268         id = self.copy(cr, uid, ids[0], context=None)
269         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
270         view_id = view_ref and view_ref[1] or False,
271         return {
272             'type': 'ir.actions.act_window',
273             'name': _('Sales Order'),
274             'res_model': 'sale.order',
275             'res_id': id,
276             'view_type': 'form',
277             'view_mode': 'form',
278             'view_id': view_id,
279             'target': 'current',
280             'nodestroy': True,
281         }
282
283     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
284         context = context or {}
285         if not pricelist_id:
286             return {}
287         value = {
288             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
289         }
290         if not order_lines:
291             return {'value': value}
292         warning = {
293             'title': _('Pricelist Warning!'),
294             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
295         }
296         return {'warning': warning, 'value': value}
297
298     def onchange_partner_id(self, cr, uid, ids, part):
299         if not part:
300             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
301
302         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
303         part = self.pool.get('res.partner').browse(cr, uid, part)
304         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
305         payment_term = part.property_payment_term and part.property_payment_term.id or False
306         fiscal_position = part.property_account_position and part.property_account_position.id or False
307         dedicated_salesman = part.user_id and part.user_id.id or uid
308         val = {
309             'partner_invoice_id': addr['invoice'],
310             'partner_shipping_id': addr['delivery'],
311             'payment_term': payment_term,
312             'fiscal_position': fiscal_position,
313             'user_id': dedicated_salesman,
314         }
315         if pricelist:
316             val['pricelist_id'] = pricelist
317         return {'value': val}
318
319     def create(self, cr, uid, vals, context=None):
320         order =  super(sale_order, self).create(cr, uid, vals, context=context)
321         if order:
322             self.create_send_note(cr, uid, [order], context=context)
323         return order
324
325     def button_dummy(self, cr, uid, ids, context=None):
326         return True
327
328     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
329     #        can be removed after 6.1.
330     def _inv_get(self, cr, uid, order, context=None):
331         return {}
332
333     def _prepare_invoice(self, cr, uid, order, lines, context=None):
334         """Prepare the dict of values to create the new invoice for a
335            sale order. This method may be overridden to implement custom
336            invoice generation (making sure to call super() to establish
337            a clean extension chain).
338
339            :param browse_record order: sale.order record to invoice
340            :param list(int) line: list of invoice line IDs that must be
341                                   attached to the invoice
342            :return: dict of value to create() the invoice
343         """
344         if context is None:
345             context = {}
346         journal_ids = self.pool.get('account.journal').search(cr, uid,
347             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
348             limit=1)
349         if not journal_ids:
350             raise osv.except_osv(_('Error!'),
351                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
352         invoice_vals = {
353             'name': order.client_order_ref or '',
354             'origin': order.name,
355             'type': 'out_invoice',
356             'reference': order.client_order_ref or order.name,
357             'account_id': order.partner_id.property_account_receivable.id,
358             'partner_id': order.partner_id.id,
359             'journal_id': journal_ids[0],
360             'invoice_line': [(6, 0, lines)],
361             'currency_id': order.pricelist_id.currency_id.id,
362             'comment': order.note,
363             'payment_term': order.payment_term and order.payment_term.id or False,
364             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
365             'date_invoice': context.get('date_invoice', False),
366             'company_id': order.company_id.id,
367             'user_id': order.user_id and order.user_id.id or False
368         }
369
370         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
371         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
372         return invoice_vals
373
374     def _make_invoice(self, cr, uid, order, lines, context=None):
375         inv_obj = self.pool.get('account.invoice')
376         obj_invoice_line = self.pool.get('account.invoice.line')
377         if context is None:
378             context = {}
379         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
380         from_line_invoice_ids = []
381         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
382             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
383                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
384                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
385         for preinv in order.invoice_ids:
386             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
387                 for preline in preinv.invoice_line:
388                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
389                     lines.append(inv_line_id)
390         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
391         inv_id = inv_obj.create(cr, uid, inv, context=context)
392         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
393         if data.get('value', False):
394             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
395         inv_obj.button_compute(cr, uid, [inv_id])
396         return inv_id
397
398     def print_quotation(self, cr, uid, ids, context=None):
399         '''
400         This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
401         '''
402         assert len(ids) == 1, 'This option should only be used for a single id at a time'
403         wf_service = netsvc.LocalService("workflow")
404         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
405         datas = {
406                  'model': 'sale.order',
407                  'ids': ids,
408                  'form': self.read(cr, uid, ids[0], context=context),
409         }
410         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
411
412     def manual_invoice(self, cr, uid, ids, context=None):
413         """ create invoices for the given sale orders (ids), and open the form
414             view of one of the newly created invoices
415         """
416         mod_obj = self.pool.get('ir.model.data')
417         wf_service = netsvc.LocalService("workflow")
418
419         # create invoices through the sale orders' workflow
420         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
421         for id in ids:
422             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
423         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
424         # determine newly created invoices
425         new_inv_ids = list(inv_ids1 - inv_ids0)
426
427         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
428         res_id = res and res[1] or False,
429
430         return {
431             'name': _('Customer Invoices'),
432             'view_type': 'form',
433             'view_mode': 'form',
434             'view_id': [res_id],
435             'res_model': 'account.invoice',
436             'context': "{'type':'out_invoice'}",
437             'type': 'ir.actions.act_window',
438             'nodestroy': True,
439             'target': 'current',
440             'res_id': new_inv_ids and new_inv_ids[0] or False,
441         }
442
443     def action_view_invoice(self, cr, uid, ids, context=None):
444         '''
445         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.
446         '''
447         mod_obj = self.pool.get('ir.model.data')
448         act_obj = self.pool.get('ir.actions.act_window')
449
450         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
451         id = result and result[1] or False
452         result = act_obj.read(cr, uid, [id], context=context)[0]
453         #compute the number of invoices to display
454         inv_ids = []
455         for so in self.browse(cr, uid, ids, context=context):
456             inv_ids += [invoice.id for invoice in so.invoice_ids]
457         #choose the view_mode accordingly
458         if len(inv_ids)>1:
459             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
460         else:
461             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
462             result['views'] = [(res and res[1] or False, 'form')]
463             result['res_id'] = inv_ids and inv_ids[0] or False
464         return result
465
466     def test_no_product(self, cr, uid, order, context):
467         for line in order.order_line:
468             if line.product_id and (line.product_id.type<>'service'):
469                 return False
470         return True
471
472     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
473         if states is None:
474             states = ['confirmed', 'done', 'exception']
475         res = False
476         invoices = {}
477         invoice_ids = []
478         invoice = self.pool.get('account.invoice')
479         obj_sale_order_line = self.pool.get('sale.order.line')
480         partner_currency = {}
481         if context is None:
482             context = {}
483         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
484         # last day of the last month as invoice date
485         if date_inv:
486             context['date_inv'] = date_inv
487         for o in self.browse(cr, uid, ids, context=context):
488             currency_id = o.pricelist_id.currency_id.id
489             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
490                 raise osv.except_osv(
491                     _('Error!'),
492                     _('You cannot group sales having different currencies for the same partner.'))
493
494             partner_currency[o.partner_id.id] = currency_id
495             lines = []
496             for line in o.order_line:
497                 if line.invoiced:
498                     continue
499                 elif (line.state in states):
500                     lines.append(line.id)
501             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
502             if created_lines:
503                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
504         if not invoices:
505             for o in self.browse(cr, uid, ids, context=context):
506                 for i in o.invoice_ids:
507                     if i.state == 'draft':
508                         return i.id
509         for val in invoices.values():
510             if grouped:
511                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
512                 invoice_ref = ''
513                 for o, l in val:
514                     invoice_ref += o.name + '|'
515                     self.write(cr, uid, [o.id], {'state': 'progress'})
516                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
517                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
518             else:
519                 for order, il in val:
520                     res = self._make_invoice(cr, uid, order, il, context=context)
521                     invoice_ids.append(res)
522                     self.write(cr, uid, [order.id], {'state': 'progress'})
523                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
524         if res:
525             self.invoice_send_note(cr, uid, ids, res, context)
526         return res
527
528     def action_invoice_cancel(self, cr, uid, ids, context=None):
529         if context is None:
530             context = {}
531         for sale in self.browse(cr, uid, ids, context=context):
532             for line in sale.order_line:
533                 #
534                 # Check if the line is invoiced (has asociated invoice
535                 # lines from non-cancelled invoices).
536                 #
537                 invoiced = False
538                 for iline in line.invoice_lines:
539                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
540                         invoiced = True
541                         break
542                 # Update the line (only when needed)
543                 if line.invoiced != invoiced:
544                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
545         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
546         return True
547
548     def action_invoice_end(self, cr, uid, ids, context=None):
549         for order in self.browse(cr, uid, ids, context=context):
550             #
551             # Update the sale order lines state (and invoiced flag).
552             #
553             for line in order.order_line:
554                 vals = {}
555                 #
556                 # Check if the line is invoiced (has asociated invoice
557                 # lines from non-cancelled invoices).
558                 #
559                 invoiced = False
560                 for iline in line.invoice_lines:
561                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
562                         invoiced = True
563                         break
564                 if line.invoiced != invoiced:
565                     vals['invoiced'] = invoiced
566                 # If the line was in exception state, now it gets confirmed.
567                 if line.state == 'exception':
568                     vals['state'] = 'confirmed'
569                 # Update the line (only when needed).
570                 if vals:
571                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
572             #
573             # Update the sales order state.
574             #
575             if order.state == 'invoice_except':
576                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
577             self.invoice_paid_send_note(cr, uid, [order.id], context=context)
578         return True
579
580     def action_cancel(self, cr, uid, ids, context=None):
581         wf_service = netsvc.LocalService("workflow")
582         if context is None:
583             context = {}
584         sale_order_line_obj = self.pool.get('sale.order.line')
585         for sale in self.browse(cr, uid, ids, context=context):
586             for inv in sale.invoice_ids:
587                 if inv.state not in ('draft', 'cancel'):
588                     raise osv.except_osv(
589                         _('Cannot cancel this sales order!'),
590                         _('First cancel all invoices attached to this sales order.'))
591             for r in self.read(cr, uid, ids, ['invoice_ids']):
592                 for inv in r['invoice_ids']:
593                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
594             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
595                     {'state': 'cancel'})
596             self.cancel_send_note(cr, uid, [sale.id], context=None)
597         self.write(cr, uid, ids, {'state': 'cancel'})
598         return True
599
600     def action_button_confirm(self, cr, uid, ids, context=None):
601         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
602         wf_service = netsvc.LocalService('workflow')
603         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
604
605         # redisplay the record as a sale order
606         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
607         view_id = view_ref and view_ref[1] or False,
608         return {
609             'type': 'ir.actions.act_window',
610             'name': _('Sales Order'),
611             'res_model': 'sale.order',
612             'res_id': ids[0],
613             'view_type': 'form',
614             'view_mode': 'form',
615             'view_id': view_id,
616             'target': 'current',
617             'nodestroy': True,
618         }
619
620     def action_wait(self, cr, uid, ids, context=None):
621         context = context or {}
622         for o in self.browse(cr, uid, ids):
623             if not o.order_line:
624                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
625             noprod = self.test_no_product(cr, uid, o, context)
626             if (o.order_policy == 'manual') or noprod:
627                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
628             else:
629                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
630             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
631             self.confirm_send_note(cr, uid, ids, context)
632         return True
633
634     def action_quotation_send(self, cr, uid, ids, context=None):
635         '''
636         This function opens a window to compose an email, with the edi sale template message loaded by default
637         '''
638         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
639         ir_model_data = self.pool.get('ir.model.data')
640         try:
641             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
642         except ValueError:
643             template_id = False
644         try:
645             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
646         except ValueError:
647             compose_form_id = False 
648         ctx = dict(context)
649         ctx.update({
650             'default_model': 'sale.order',
651             'default_res_id': ids[0],
652             'default_use_template': bool(template_id),
653             'default_template_id': template_id,
654             'default_composition_mode': 'comment',
655             'mark_so_as_sent': True
656         })
657         return {
658             'type': 'ir.actions.act_window',
659             'view_type': 'form',
660             'view_mode': 'form',
661             'res_model': 'mail.compose.message',
662             'views': [(compose_form_id, 'form')],
663             'view_id': compose_form_id,
664             'target': 'new',
665             'context': ctx,
666         }
667
668     def action_done(self, cr, uid, ids, context=None):
669         self.done_send_note(cr, uid, ids, context=context)
670         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
671
672     # ------------------------------------------------
673     # OpenChatter methods and notifications
674     # ------------------------------------------------
675
676     def needaction_domain_get(self, cr, uid, ids, context=None):
677         return [('state', '=', 'draft'), ('user_id','=',uid)]
678
679     def create_send_note(self, cr, uid, ids, context=None):
680         for obj in self.browse(cr, uid, ids, context=context):
681             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
682
683     def confirm_send_note(self, cr, uid, ids, context=None):
684         for obj in self.browse(cr, uid, ids, context=context):
685             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), subtype="sale.mt_order_confirmed", context=context)
686
687     def cancel_send_note(self, cr, uid, ids, context=None):
688         for obj in self.browse(cr, uid, ids, context=context):
689             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
690
691     def done_send_note(self, cr, uid, ids, context=None):
692         for obj in self.browse(cr, uid, ids, context=context):
693             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> set to <b>Done</b>") % (obj.partner_id.name), context=context)
694
695     def invoice_paid_send_note(self, cr, uid, ids, context=None):
696         self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
697
698     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
699         for order in self.browse(cr, uid, ids, context=context):
700             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
701                 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
702
703 sale_order()
704
705 # TODO add a field price_unit_uos
706 # - update it on change product and unit price
707 # - use it in report if there is a uos
708 class sale_order_line(osv.osv):
709
710     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
711         tax_obj = self.pool.get('account.tax')
712         cur_obj = self.pool.get('res.currency')
713         res = {}
714         if context is None:
715             context = {}
716         for line in self.browse(cr, uid, ids, context=context):
717             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
718             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
719             cur = line.order_id.pricelist_id.currency_id
720             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
721         return res
722
723     def _get_uom_id(self, cr, uid, *args):
724         try:
725             proxy = self.pool.get('ir.model.data')
726             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
727             return result[1]
728         except Exception, ex:
729             return False
730
731     _name = 'sale.order.line'
732     _description = 'Sales Order Line'
733     _columns = {
734         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
735         'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
736         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
737         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
738         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
739         'invoiced': fields.boolean('Invoiced', readonly=True),
740         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
741         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
742          help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."),
743         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
744         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
745         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
746         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
747         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
748         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
749         'product_uos': fields.many2one('product.uom', 'Product UoS'),
750         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
751         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
752         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
753                 help='* The \'Draft\' status is set when the related sales order in draft status. \
754                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
755                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
756                     \n* The \'Done\' status is set when the sales order line has been picked. \
757                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
758         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
759         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
760         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
761     }
762     _order = 'sequence, id'
763     _defaults = {
764         'product_uom' : _get_uom_id,
765         'discount': 0.0,
766         'product_uom_qty': 1,
767         'product_uos_qty': 1,
768         'sequence': 10,
769         'invoiced': 0,
770         'state': 'draft',
771         'type': 'make_to_stock',
772         'price_unit': 0.0,
773     }
774
775     def _get_line_qty(self, cr, uid, line, context=None):
776         if (line.order_id.invoice_quantity=='order'):
777             if line.product_uos:
778                 return line.product_uos_qty or 0.0
779         return line.product_uom_qty
780
781     def _get_line_uom(self, cr, uid, line, context=None):
782         if (line.order_id.invoice_quantity=='order'):
783             if line.product_uos:
784                 return line.product_uos.id
785         return line.product_uom.id
786
787     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
788         """Prepare the dict of values to create the new invoice line for a
789            sale order line. This method may be overridden to implement custom
790            invoice generation (making sure to call super() to establish
791            a clean extension chain).
792
793            :param browse_record line: sale.order.line record to invoice
794            :param int account_id: optional ID of a G/L account to force
795                (this is used for returning products including service)
796            :return: dict of values to create() the invoice line
797         """
798         res = {}
799         if not line.invoiced:
800             if not account_id:
801                 if line.product_id:
802                     account_id = line.product_id.product_tmpl_id.property_account_income.id
803                     if not account_id:
804                         account_id = line.product_id.categ_id.property_account_income_categ.id
805                     if not account_id:
806                         raise osv.except_osv(_('Error!'),
807                                 _('Please define income account for this product: "%s" (id:%d).') % \
808                                     (line.product_id.name, line.product_id.id,))
809                 else:
810                     prop = self.pool.get('ir.property').get(cr, uid,
811                             'property_account_income_categ', 'product.category',
812                             context=context)
813                     account_id = prop and prop.id or False
814             uosqty = self._get_line_qty(cr, uid, line, context=context)
815             uos_id = self._get_line_uom(cr, uid, line, context=context)
816             pu = 0.0
817             if uosqty:
818                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
819                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
820             fpos = line.order_id.fiscal_position or False
821             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
822             if not account_id:
823                 raise osv.except_osv(_('Error!'),
824                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
825             res = {
826                 'name': line.name,
827                 'sequence': line.sequence,
828                 'origin': line.order_id.name,
829                 'account_id': account_id,
830                 'price_unit': pu,
831                 'quantity': uosqty,
832                 'discount': line.discount,
833                 'uos_id': uos_id,
834                 'product_id': line.product_id.id or False,
835                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
836                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
837             }
838
839         return res
840
841     def invoice_line_create(self, cr, uid, ids, context=None):
842         if context is None:
843             context = {}
844
845         create_ids = []
846         sales = set()
847         for line in self.browse(cr, uid, ids, context=context):
848             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
849             if vals:
850                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
851                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
852                 self.write(cr, uid, [line.id], {'invoiced': True})
853                 sales.add(line.order_id.id)
854                 create_ids.append(inv_id)
855         # Trigger workflow events
856         wf_service = netsvc.LocalService("workflow")
857         for sale_id in sales:
858             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
859         return create_ids
860
861     def button_cancel(self, cr, uid, ids, context=None):
862         for line in self.browse(cr, uid, ids, context=context):
863             if line.invoiced:
864                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
865         return self.write(cr, uid, ids, {'state': 'cancel'})
866
867     def button_confirm(self, cr, uid, ids, context=None):
868         return self.write(cr, uid, ids, {'state': 'confirmed'})
869
870     def button_done(self, cr, uid, ids, context=None):
871         wf_service = netsvc.LocalService("workflow")
872         res = self.write(cr, uid, ids, {'state': 'done'})
873         for line in self.browse(cr, uid, ids, context=context):
874             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
875         return res
876
877     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
878         product_obj = self.pool.get('product.product')
879         if not product_id:
880             return {'value': {'product_uom': product_uos,
881                 'product_uom_qty': product_uos_qty}, 'domain': {}}
882
883         product = product_obj.browse(cr, uid, product_id)
884         value = {
885             'product_uom': product.uom_id.id,
886         }
887         # FIXME must depend on uos/uom of the product and not only of the coeff.
888         try:
889             value.update({
890                 'product_uom_qty': product_uos_qty / product.uos_coeff,
891                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
892             })
893         except ZeroDivisionError:
894             pass
895         return {'value': value}
896
897     def copy_data(self, cr, uid, id, default=None, context=None):
898         if not default:
899             default = {}
900         default.update({'state': 'draft',  'invoiced': False, 'invoice_lines': []})
901         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
902
903     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
904             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
905             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
906         context = context or {}
907         lang = lang or context.get('lang',False)
908         if not  partner_id:
909             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
910         warning = {}
911         product_uom_obj = self.pool.get('product.uom')
912         partner_obj = self.pool.get('res.partner')
913         product_obj = self.pool.get('product.product')
914         context = {'lang': lang, 'partner_id': partner_id}
915         if partner_id:
916             lang = partner_obj.browse(cr, uid, partner_id).lang
917         context_partner = {'lang': lang, 'partner_id': partner_id}
918
919         if not product:
920             return {'value': {'th_weight': 0,
921                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
922                    'product_uos': []}}
923         if not date_order:
924             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
925
926         result = {}
927         warning_msgs = {}
928         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
929
930         uom2 = False
931         if uom:
932             uom2 = product_uom_obj.browse(cr, uid, uom)
933             if product_obj.uom_id.category_id.id != uom2.category_id.id:
934                 uom = False
935         if uos:
936             if product_obj.uos_id:
937                 uos2 = product_uom_obj.browse(cr, uid, uos)
938                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
939                     uos = False
940             else:
941                 uos = False
942         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
943         if update_tax: #The quantity only have changed
944             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
945
946         if not flag:
947             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
948             if product_obj.description_sale:
949                 result['name'] += '\n'+product_obj.description_sale
950         domain = {}
951         if (not uom) and (not uos):
952             result['product_uom'] = product_obj.uom_id.id
953             if product_obj.uos_id:
954                 result['product_uos'] = product_obj.uos_id.id
955                 result['product_uos_qty'] = qty * product_obj.uos_coeff
956                 uos_category_id = product_obj.uos_id.category_id.id
957             else:
958                 result['product_uos'] = False
959                 result['product_uos_qty'] = qty
960                 uos_category_id = False
961             result['th_weight'] = qty * product_obj.weight
962             domain = {'product_uom':
963                         [('category_id', '=', product_obj.uom_id.category_id.id)],
964                         'product_uos':
965                         [('category_id', '=', uos_category_id)]}
966         elif uos and not uom: # only happens if uom is False
967             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
968             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
969             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
970         elif uom: # whether uos is set or not
971             default_uom = product_obj.uom_id and product_obj.uom_id.id
972             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
973             result['product_uom'] = default_uom
974             if product_obj.uos_id:
975                 result['product_uos'] = product_obj.uos_id.id
976                 result['product_uos_qty'] = qty * product_obj.uos_coeff
977             else:
978                 result['product_uos'] = False
979                 result['product_uos_qty'] = qty
980             result['th_weight'] = q * product_obj.weight        # Round the quantity up
981
982         if not uom2:
983             uom2 = product_obj.uom_id
984         # get unit price
985
986         if not pricelist:
987             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
988                     'Please set one before choosing a product.')
989             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
990         else:
991             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
992                     product, qty or 1.0, partner_id, {
993                         'uom': uom or result.get('product_uom'),
994                         'date': date_order,
995                         })[pricelist]
996             if price is False:
997                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
998                         "You have to change either the product, the quantity or the pricelist.")
999
1000                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1001             else:
1002                 result.update({'price_unit': price})
1003         if warning_msgs:
1004             warning = {
1005                        'title': _('Configuration Error!'),
1006                        'message' : warning_msgs
1007                     }
1008         return {'value': result, 'domain': domain, 'warning': warning}
1009
1010     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1011             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1012             lang=False, update_tax=True, date_order=False, context=None):
1013         context = context or {}
1014         lang = lang or ('lang' in context and context['lang'])
1015         if not uom:
1016             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1017         return self.product_id_change(cursor, user, ids, pricelist, product,
1018                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1019                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1020                 date_order=date_order, context=context)
1021
1022     def unlink(self, cr, uid, ids, context=None):
1023         if context is None:
1024             context = {}
1025         """Allows to delete sales order lines in draft,cancel states"""
1026         for rec in self.browse(cr, uid, ids, context=context):
1027             if rec.state not in ['draft', 'cancel']:
1028                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1029         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1030
1031
1032 class mail_compose_message(osv.osv):
1033     _inherit = 'mail.compose.message'
1034     def send_mail(self, cr, uid, ids, context=None):
1035         context = context or {}
1036         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1037             wf_service = netsvc.LocalService("workflow")
1038             wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1039         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1040
1041 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: