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