5fe84c34bc39736dac3e277f80613f77f49c1770
[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         if not pricelist_id:
285             return {}
286         value = {
287             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
288         }
289         if not order_lines:
290             return {'value': value}
291         warning = {
292             'title': _('Pricelist Warning!'),
293             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
294         }
295         return {'warning': warning, 'value': value}
296
297     def onchange_partner_id(self, cr, uid, ids, part):
298         if not part:
299             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
300
301         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
302         part = self.pool.get('res.partner').browse(cr, uid, part)
303         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
304         payment_term = part.property_payment_term and part.property_payment_term.id or False
305         fiscal_position = part.property_account_position and part.property_account_position.id or False
306         dedicated_salesman = part.user_id and part.user_id.id or uid
307         val = {
308             'partner_invoice_id': addr['invoice'],
309             'partner_shipping_id': addr['delivery'],
310             'payment_term': payment_term,
311             'fiscal_position': fiscal_position,
312             'user_id': dedicated_salesman,
313         }
314         if pricelist:
315             val['pricelist_id'] = pricelist
316         return {'value': val}
317
318     def create(self, cr, uid, vals, context=None):
319         order =  super(sale_order, self).create(cr, uid, vals, context=context)
320         if order:
321             self.create_send_note(cr, uid, [order], context=context)
322         return order
323
324     def button_dummy(self, cr, uid, ids, context=None):
325         return True
326
327     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
328     #        can be removed after 6.1.
329     def _inv_get(self, cr, uid, order, context=None):
330         return {}
331
332     def _prepare_invoice(self, cr, uid, order, lines, context=None):
333         """Prepare the dict of values to create the new invoice for a
334            sale order. This method may be overridden to implement custom
335            invoice generation (making sure to call super() to establish
336            a clean extension chain).
337
338            :param browse_record order: sale.order record to invoice
339            :param list(int) line: list of invoice line IDs that must be
340                                   attached to the invoice
341            :return: dict of value to create() the invoice
342         """
343         if context is None:
344             context = {}
345         journal_ids = self.pool.get('account.journal').search(cr, uid,
346             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
347             limit=1)
348         if not journal_ids:
349             raise osv.except_osv(_('Error!'),
350                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
351         invoice_vals = {
352             'name': order.client_order_ref or '',
353             'origin': order.name,
354             'type': 'out_invoice',
355             'reference': order.client_order_ref or order.name,
356             'account_id': order.partner_id.property_account_receivable.id,
357             'partner_id': order.partner_id.id,
358             'journal_id': journal_ids[0],
359             'invoice_line': [(6, 0, lines)],
360             'currency_id': order.pricelist_id.currency_id.id,
361             'comment': order.note,
362             'payment_term': order.payment_term and order.payment_term.id or False,
363             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
364             'date_invoice': context.get('date_invoice', False),
365             'company_id': order.company_id.id,
366             'user_id': order.user_id and order.user_id.id or False
367         }
368
369         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
370         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
371         return invoice_vals
372
373     def _make_invoice(self, cr, uid, order, lines, context=None):
374         inv_obj = self.pool.get('account.invoice')
375         obj_invoice_line = self.pool.get('account.invoice.line')
376         if context is None:
377             context = {}
378         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
379         from_line_invoice_ids = []
380         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
381             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
382                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
383                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
384         for preinv in order.invoice_ids:
385             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
386                 for preline in preinv.invoice_line:
387                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
388                     lines.append(inv_line_id)
389         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
390         inv_id = inv_obj.create(cr, uid, inv, context=context)
391         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
392         if data.get('value', False):
393             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
394         inv_obj.button_compute(cr, uid, [inv_id])
395         return inv_id
396
397     def print_quotation(self, cr, uid, ids, context=None):
398         '''
399         This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
400         '''
401         assert len(ids) == 1, 'This option should only be used for a single id at a time'
402         wf_service = netsvc.LocalService("workflow")
403         wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
404         datas = {
405                  'model': 'sale.order',
406                  'ids': ids,
407                  'form': self.read(cr, uid, ids[0], context=context),
408         }
409         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
410
411     def manual_invoice(self, cr, uid, ids, context=None):
412         """ create invoices for the given sale orders (ids), and open the form
413             view of one of the newly created invoices
414         """
415         mod_obj = self.pool.get('ir.model.data')
416         wf_service = netsvc.LocalService("workflow")
417
418         # create invoices through the sale orders' workflow
419         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
420         for id in ids:
421             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
422         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
423         # determine newly created invoices
424         new_inv_ids = list(inv_ids1 - inv_ids0)
425
426         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
427         res_id = res and res[1] or False,
428
429         return {
430             'name': _('Customer Invoices'),
431             'view_type': 'form',
432             'view_mode': 'form',
433             'view_id': [res_id],
434             'res_model': 'account.invoice',
435             'context': "{'type':'out_invoice'}",
436             'type': 'ir.actions.act_window',
437             'nodestroy': True,
438             'target': 'current',
439             'res_id': new_inv_ids and new_inv_ids[0] or False,
440         }
441
442     def action_view_invoice(self, cr, uid, ids, context=None):
443         '''
444         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.
445         '''
446         mod_obj = self.pool.get('ir.model.data')
447         act_obj = self.pool.get('ir.actions.act_window')
448
449         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
450         id = result and result[1] or False
451         result = act_obj.read(cr, uid, [id], context=context)[0]
452         #compute the number of invoices to display
453         inv_ids = []
454         for so in self.browse(cr, uid, ids, context=context):
455             inv_ids += [invoice.id for invoice in so.invoice_ids]
456         #choose the view_mode accordingly
457         if len(inv_ids)>1:
458             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
459         else:
460             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
461             result['views'] = [(res and res[1] or False, 'form')]
462             result['res_id'] = inv_ids and inv_ids[0] or False
463         return result
464
465     def test_no_product(self, cr, uid, order, context):
466         for line in order.order_line:
467             if line.product_id and (line.product_id.type<>'service'):
468                 return False
469         return True
470
471     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
472         if states is None:
473             states = ['confirmed', 'done', 'exception']
474         res = False
475         invoices = {}
476         invoice_ids = []
477         invoice = self.pool.get('account.invoice')
478         obj_sale_order_line = self.pool.get('sale.order.line')
479         partner_currency = {}
480         if context is None:
481             context = {}
482         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
483         # last day of the last month as invoice date
484         if date_inv:
485             context['date_inv'] = date_inv
486         for o in self.browse(cr, uid, ids, context=context):
487             currency_id = o.pricelist_id.currency_id.id
488             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
489                 raise osv.except_osv(
490                     _('Error!'),
491                     _('You cannot group sales having different currencies for the same partner.'))
492
493             partner_currency[o.partner_id.id] = currency_id
494             lines = []
495             for line in o.order_line:
496                 if line.invoiced:
497                     continue
498                 elif (line.state in states):
499                     lines.append(line.id)
500             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
501             if created_lines:
502                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
503         if not invoices:
504             for o in self.browse(cr, uid, ids, context=context):
505                 for i in o.invoice_ids:
506                     if i.state == 'draft':
507                         return i.id
508         for val in invoices.values():
509             if grouped:
510                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
511                 invoice_ref = ''
512                 for o, l in val:
513                     invoice_ref += o.name + '|'
514                     self.write(cr, uid, [o.id], {'state': 'progress'})
515                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
516                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
517             else:
518                 for order, il in val:
519                     res = self._make_invoice(cr, uid, order, il, context=context)
520                     invoice_ids.append(res)
521                     self.write(cr, uid, [order.id], {'state': 'progress'})
522                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
523         if res:
524             self.invoice_send_note(cr, uid, ids, res, context)
525         return res
526
527     def action_invoice_cancel(self, cr, uid, ids, context=None):
528         if context is None:
529             context = {}
530         for sale in self.browse(cr, uid, ids, context=context):
531             for line in sale.order_line:
532                 #
533                 # Check if the line is invoiced (has asociated invoice
534                 # lines from non-cancelled invoices).
535                 #
536                 invoiced = False
537                 for iline in line.invoice_lines:
538                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
539                         invoiced = True
540                         break
541                 # Update the line (only when needed)
542                 if line.invoiced != invoiced:
543                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
544         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
545         return True
546
547     def action_invoice_end(self, cr, uid, ids, context=None):
548         for order in self.browse(cr, uid, ids, context=context):
549             #
550             # Update the sale order lines state (and invoiced flag).
551             #
552             for line in order.order_line:
553                 vals = {}
554                 #
555                 # Check if the line is invoiced (has asociated invoice
556                 # lines from non-cancelled invoices).
557                 #
558                 invoiced = False
559                 for iline in line.invoice_lines:
560                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
561                         invoiced = True
562                         break
563                 if line.invoiced != invoiced:
564                     vals['invoiced'] = invoiced
565                 # If the line was in exception state, now it gets confirmed.
566                 if line.state == 'exception':
567                     vals['state'] = 'confirmed'
568                 # Update the line (only when needed).
569                 if vals:
570                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
571             #
572             # Update the sales order state.
573             #
574             if order.state == 'invoice_except':
575                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
576             self.invoice_paid_send_note(cr, uid, [order.id], context=context)
577         return True
578
579     def action_cancel(self, cr, uid, ids, context=None):
580         wf_service = netsvc.LocalService("workflow")
581         if context is None:
582             context = {}
583         sale_order_line_obj = self.pool.get('sale.order.line')
584         for sale in self.browse(cr, uid, ids, context=context):
585             for inv in sale.invoice_ids:
586                 if inv.state not in ('draft', 'cancel'):
587                     raise osv.except_osv(
588                         _('Cannot cancel this sales order!'),
589                         _('First cancel all invoices attached to this sales order.'))
590             for r in self.read(cr, uid, ids, ['invoice_ids']):
591                 for inv in r['invoice_ids']:
592                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
593             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
594                     {'state': 'cancel'})
595             self.cancel_send_note(cr, uid, [sale.id], context=None)
596         self.write(cr, uid, ids, {'state': 'cancel'})
597         return True
598
599     def action_button_confirm(self, cr, uid, ids, context=None):
600         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
601         wf_service = netsvc.LocalService('workflow')
602         wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
603
604         # redisplay the record as a sale order
605         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
606         view_id = view_ref and view_ref[1] or False,
607         return {
608             'type': 'ir.actions.act_window',
609             'name': _('Sales Order'),
610             'res_model': 'sale.order',
611             'res_id': ids[0],
612             'view_type': 'form',
613             'view_mode': 'form',
614             'view_id': view_id,
615             'target': 'current',
616             'nodestroy': True,
617         }
618
619     def action_wait(self, cr, uid, ids, context=None):
620         for o in self.browse(cr, uid, ids):
621             if not o.order_line:
622                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
623             noprod = self.test_no_product(cr, uid, o, context)
624             if (o.order_policy == 'manual') or noprod:
625                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
626             else:
627                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
628             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
629             self.confirm_send_note(cr, uid, ids, context)
630         return True
631
632     def action_quotation_send(self, cr, uid, ids, context=None):
633         '''
634         This function opens a window to compose an email, with the edi sale template message loaded by default
635         '''
636         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
637         mod_obj = self.pool.get('ir.model.data')
638         template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
639         template_id = template and template[1] or False
640         res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
641         res_id = res and res[1] or False
642         ctx = dict(context)
643         ctx.update({
644             'default_model': 'sale.order',
645             'default_res_id': ids[0],
646             'default_use_template': True,
647             'default_template_id': template_id,
648             'default_composition_mode': 'comment',
649             'mark_so_as_sent': True
650         })
651         return {
652             'view_type': 'form',
653             'view_mode': 'form',
654             'res_model': 'mail.compose.message',
655             'views': [(res_id, 'form')],
656             'view_id': res_id,
657             'type': 'ir.actions.act_window',
658             'target': 'new',
659             'context': ctx,
660             'nodestroy': True,
661         }
662
663     def action_done(self, cr, uid, ids, context=None):
664         self.done_send_note(cr, uid, ids, context=context)
665         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
666
667     # ------------------------------------------------
668     # OpenChatter methods and notifications
669     # ------------------------------------------------
670
671     def needaction_domain_get(self, cr, uid, ids, context=None):
672         return [('state', '=', 'draft'), ('user_id','=',uid)]
673
674     def create_send_note(self, cr, uid, ids, context=None):
675         for obj in self.browse(cr, uid, ids, context=context):
676             self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
677
678     def confirm_send_note(self, cr, uid, ids, context=None):
679         for obj in self.browse(cr, uid, ids, context=context):
680             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), context=context)
681
682     def cancel_send_note(self, cr, uid, ids, context=None):
683         for obj in self.browse(cr, uid, ids, context=context):
684             self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
685
686     def done_send_note(self, cr, uid, ids, context=None):
687         for obj in self.browse(cr, uid, ids, context=context):
688             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)
689
690     def invoice_paid_send_note(self, cr, uid, ids, context=None):
691         self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
692
693     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
694         for order in self.browse(cr, uid, ids, context=context):
695             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
696                 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)
697
698 sale_order()
699
700 # TODO add a field price_unit_uos
701 # - update it on change product and unit price
702 # - use it in report if there is a uos
703 class sale_order_line(osv.osv):
704
705     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
706         tax_obj = self.pool.get('account.tax')
707         cur_obj = self.pool.get('res.currency')
708         res = {}
709         if context is None:
710             context = {}
711         for line in self.browse(cr, uid, ids, context=context):
712             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
713             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
714             cur = line.order_id.pricelist_id.currency_id
715             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
716         return res
717
718     def _get_uom_id(self, cr, uid, *args):
719         try:
720             proxy = self.pool.get('ir.model.data')
721             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
722             return result[1]
723         except Exception, ex:
724             return False
725
726     _name = 'sale.order.line'
727     _description = 'Sales Order Line'
728     _columns = {
729         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
730         'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
731         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
732         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
733         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
734         'invoiced': fields.boolean('Invoiced', readonly=True),
735         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
736         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
737          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."),
738         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
739         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
740         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
741         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
742         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
743         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
744         'product_uos': fields.many2one('product.uom', 'Product UoS'),
745         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
746         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
747         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
748                 help='* The \'Draft\' status is set when the related sales order in draft status. \
749                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
750                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
751                     \n* The \'Done\' status is set when the sales order line has been picked. \
752                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
753         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
754         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
755         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
756     }
757     _order = 'sequence, id'
758     _defaults = {
759         'product_uom' : _get_uom_id,
760         'discount': 0.0,
761         'product_uom_qty': 1,
762         'product_uos_qty': 1,
763         'sequence': 10,
764         'invoiced': 0,
765         'state': 'draft',
766         'type': 'make_to_stock',
767         'price_unit': 0.0,
768     }
769
770     def _get_line_qty(self, cr, uid, line, context=None):
771         if (line.order_id.invoice_quantity=='order'):
772             if line.product_uos:
773                 return line.product_uos_qty or 0.0
774         return line.product_uom_qty
775
776     def _get_line_uom(self, cr, uid, line, context=None):
777         if (line.order_id.invoice_quantity=='order'):
778             if line.product_uos:
779                 return line.product_uos.id
780         return line.product_uom.id
781
782     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
783         """Prepare the dict of values to create the new invoice line for a
784            sale order line. This method may be overridden to implement custom
785            invoice generation (making sure to call super() to establish
786            a clean extension chain).
787
788            :param browse_record line: sale.order.line record to invoice
789            :param int account_id: optional ID of a G/L account to force
790                (this is used for returning products including service)
791            :return: dict of values to create() the invoice line
792         """
793         res = {}
794         if not line.invoiced:
795             if not account_id:
796                 if line.product_id:
797                     account_id = line.product_id.product_tmpl_id.property_account_income.id
798                     if not account_id:
799                         account_id = line.product_id.categ_id.property_account_income_categ.id
800                     if not account_id:
801                         raise osv.except_osv(_('Error!'),
802                                 _('Please define income account for this product: "%s" (id:%d).') % \
803                                     (line.product_id.name, line.product_id.id,))
804                 else:
805                     prop = self.pool.get('ir.property').get(cr, uid,
806                             'property_account_income_categ', 'product.category',
807                             context=context)
808                     account_id = prop and prop.id or False
809             uosqty = self._get_line_qty(cr, uid, line, context=context)
810             uos_id = self._get_line_uom(cr, uid, line, context=context)
811             pu = 0.0
812             if uosqty:
813                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
814                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
815             fpos = line.order_id.fiscal_position or False
816             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
817             if not account_id:
818                 raise osv.except_osv(_('Error!'),
819                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
820             res = {
821                 'name': line.name,
822                 'sequence': line.sequence,
823                 'origin': line.order_id.name,
824                 'account_id': account_id,
825                 'price_unit': pu,
826                 'quantity': uosqty,
827                 'discount': line.discount,
828                 'uos_id': uos_id,
829                 'product_id': line.product_id.id or False,
830                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
831                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
832             }
833
834         return res
835
836     def invoice_line_create(self, cr, uid, ids, context=None):
837         if context is None:
838             context = {}
839
840         create_ids = []
841         sales = set()
842         for line in self.browse(cr, uid, ids, context=context):
843             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
844             if vals:
845                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
846                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
847                 self.write(cr, uid, [line.id], {'invoiced': True})
848                 sales.add(line.order_id.id)
849                 create_ids.append(inv_id)
850         # Trigger workflow events
851         wf_service = netsvc.LocalService("workflow")
852         for sale_id in sales:
853             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
854         return create_ids
855
856     def button_cancel(self, cr, uid, ids, context=None):
857         for line in self.browse(cr, uid, ids, context=context):
858             if line.invoiced:
859                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
860         return self.write(cr, uid, ids, {'state': 'cancel'})
861
862     def button_confirm(self, cr, uid, ids, context=None):
863         return self.write(cr, uid, ids, {'state': 'confirmed'})
864
865     def button_done(self, cr, uid, ids, context=None):
866         wf_service = netsvc.LocalService("workflow")
867         res = self.write(cr, uid, ids, {'state': 'done'})
868         for line in self.browse(cr, uid, ids, context=context):
869             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
870         return res
871
872     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
873         product_obj = self.pool.get('product.product')
874         if not product_id:
875             return {'value': {'product_uom': product_uos,
876                 'product_uom_qty': product_uos_qty}, 'domain': {}}
877
878         product = product_obj.browse(cr, uid, product_id)
879         value = {
880             'product_uom': product.uom_id.id,
881         }
882         # FIXME must depend on uos/uom of the product and not only of the coeff.
883         try:
884             value.update({
885                 'product_uom_qty': product_uos_qty / product.uos_coeff,
886                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
887             })
888         except ZeroDivisionError:
889             pass
890         return {'value': value}
891
892     def copy_data(self, cr, uid, id, default=None, context=None):
893         if not default:
894             default = {}
895         default.update({'state': 'draft',  'invoiced': False, 'invoice_lines': []})
896         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
897
898     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
899             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
900             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
901         context = context or {}
902         lang = lang or context.get('lang',False)
903         if not  partner_id:
904             raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
905         warning = {}
906         product_uom_obj = self.pool.get('product.uom')
907         partner_obj = self.pool.get('res.partner')
908         product_obj = self.pool.get('product.product')
909         context = {'lang': lang, 'partner_id': partner_id}
910         if partner_id:
911             lang = partner_obj.browse(cr, uid, partner_id).lang
912         context_partner = {'lang': lang, 'partner_id': partner_id}
913
914         if not product:
915             return {'value': {'th_weight': 0,
916                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
917                    'product_uos': []}}
918         if not date_order:
919             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
920
921         result = {}
922         warning_msgs = {}
923         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
924
925         uom2 = False
926         if uom:
927             uom2 = product_uom_obj.browse(cr, uid, uom)
928             if product_obj.uom_id.category_id.id != uom2.category_id.id:
929                 uom = False
930         if uos:
931             if product_obj.uos_id:
932                 uos2 = product_uom_obj.browse(cr, uid, uos)
933                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
934                     uos = False
935             else:
936                 uos = False
937         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
938         if update_tax: #The quantity only have changed
939             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
940
941         if not flag:
942             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
943             if product_obj.description_sale:
944                 result['name'] += '\n'+product_obj.description_sale
945         domain = {}
946         if (not uom) and (not uos):
947             result['product_uom'] = product_obj.uom_id.id
948             if product_obj.uos_id:
949                 result['product_uos'] = product_obj.uos_id.id
950                 result['product_uos_qty'] = qty * product_obj.uos_coeff
951                 uos_category_id = product_obj.uos_id.category_id.id
952             else:
953                 result['product_uos'] = False
954                 result['product_uos_qty'] = qty
955                 uos_category_id = False
956             result['th_weight'] = qty * product_obj.weight
957             domain = {'product_uom':
958                         [('category_id', '=', product_obj.uom_id.category_id.id)],
959                         'product_uos':
960                         [('category_id', '=', uos_category_id)]}
961         elif uos and not uom: # only happens if uom is False
962             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
963             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
964             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
965         elif uom: # whether uos is set or not
966             default_uom = product_obj.uom_id and product_obj.uom_id.id
967             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
968             result['product_uom'] = default_uom
969             if product_obj.uos_id:
970                 result['product_uos'] = product_obj.uos_id.id
971                 result['product_uos_qty'] = qty * product_obj.uos_coeff
972             else:
973                 result['product_uos'] = False
974                 result['product_uos_qty'] = qty
975             result['th_weight'] = q * product_obj.weight        # Round the quantity up
976
977         if not uom2:
978             uom2 = product_obj.uom_id
979         # get unit price
980
981         if not pricelist:
982             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
983                     'Please set one before choosing a product.')
984             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
985         else:
986             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
987                     product, qty or 1.0, partner_id, {
988                         'uom': uom or result.get('product_uom'),
989                         'date': date_order,
990                         })[pricelist]
991             if price is False:
992                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
993                         "You have to change either the product, the quantity or the pricelist.")
994
995                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
996             else:
997                 result.update({'price_unit': price})
998         if warning_msgs:
999             warning = {
1000                        'title': _('Configuration Error!'),
1001                        'message' : warning_msgs
1002                     }
1003         return {'value': result, 'domain': domain, 'warning': warning}
1004
1005     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1006             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1007             lang=False, update_tax=True, date_order=False, context=None):
1008         context = context or {}
1009         lang = lang or ('lang' in context and context['lang'])
1010         if not uom:
1011             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1012         return self.product_id_change(cursor, user, ids, pricelist, product,
1013                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1014                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1015                 date_order=date_order, context=context)
1016
1017     def unlink(self, cr, uid, ids, context=None):
1018         if context is None:
1019             context = {}
1020         """Allows to delete sales order lines in draft,cancel states"""
1021         for rec in self.browse(cr, uid, ids, context=context):
1022             if rec.state not in ['draft', 'cancel']:
1023                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1024         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1025
1026 sale_order_line()
1027
1028 class mail_compose_message(osv.osv):
1029     _inherit = 'mail.compose.message'
1030     def send_mail(self, cr, uid, ids, context=None):
1031         context = context or {}
1032         if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1033             wf_service = netsvc.LocalService("workflow")
1034             wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1035         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1036
1037 mail_compose_message()
1038
1039 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: