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