[MERGE] forward port of branch 8.0 up to e883193
[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 import time
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
27 import openerp.addons.decimal_precision as dp
28 from openerp import workflow
29
30 class res_company(osv.Model):
31     _inherit = "res.company"
32     _columns = {
33         'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
34     }
35
36 class sale_order(osv.osv):
37     _name = "sale.order"
38     _inherit = ['mail.thread', 'ir.needaction_mixin']
39     _description = "Sales Order"
40     _track = {
41         'state': {
42             'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
43             'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
44         },
45     }
46
47     def _amount_line_tax(self, cr, uid, line, context=None):
48         val = 0.0
49         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']:
50             val += c.get('amount', 0.0)
51         return val
52
53     def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
54         """ Wrapper because of direct method passing as parameter for function fields """
55         return self._amount_all(cr, uid, ids, field_name, arg, context=context)
56
57     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
58         cur_obj = self.pool.get('res.currency')
59         res = {}
60         for order in self.browse(cr, uid, ids, context=context):
61             res[order.id] = {
62                 'amount_untaxed': 0.0,
63                 'amount_tax': 0.0,
64                 'amount_total': 0.0,
65             }
66             val = val1 = 0.0
67             cur = order.pricelist_id.currency_id
68             for line in order.order_line:
69                 val1 += line.price_subtotal
70                 val += self._amount_line_tax(cr, uid, line, context=context)
71             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
72             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
73             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
74         return res
75
76
77     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
78         res = {}
79         for sale in self.browse(cursor, user, ids, context=context):
80             if sale.invoiced:
81                 res[sale.id] = 100.0
82                 continue
83             tot = 0.0
84             for invoice in sale.invoice_ids:
85                 if invoice.state not in ('draft', 'cancel'):
86                     tot += invoice.amount_untaxed
87             if tot:
88                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
89             else:
90                 res[sale.id] = 0.0
91         return res
92
93     def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
94         res = {}
95         for sale in self.browse(cursor, user, ids, context=context):
96             res[sale.id] = False
97             if sale.invoice_ids:
98                 res[sale.id] = True
99         return res
100
101     def _invoiced(self, cursor, user, ids, name, arg, context=None):
102         res = {}
103         for sale in self.browse(cursor, user, ids, context=context):
104             res[sale.id] = True
105             invoice_existence = False
106             for invoice in sale.invoice_ids:
107                 if invoice.state!='cancel':
108                     invoice_existence = True
109                     if invoice.state != 'paid':
110                         res[sale.id] = False
111                         break
112             if not invoice_existence or sale.state == 'manual':
113                 res[sale.id] = False
114         return res
115
116     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
117         if not len(args):
118             return []
119         clause = ''
120         sale_clause = ''
121         no_invoiced = False
122         for arg in args:
123             if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
124                 clause += 'AND inv.state = \'paid\''
125             else:
126                 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
127                 sale_clause = ',  sale_order AS sale '
128                 no_invoiced = True
129
130         cursor.execute('SELECT rel.order_id ' \
131                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
132                 'WHERE rel.invoice_id = inv.id ' + clause)
133         res = cursor.fetchall()
134         if no_invoiced:
135             cursor.execute('SELECT sale.id ' \
136                     'FROM sale_order AS sale ' \
137                     'WHERE sale.id NOT IN ' \
138                         '(SELECT rel.order_id ' \
139                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
140             res.extend(cursor.fetchall())
141         if not res:
142             return [('id', '=', 0)]
143         return [('id', 'in', [x[0] for x in res])]
144
145     def _get_order(self, cr, uid, ids, context=None):
146         result = {}
147         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
148             result[line.order_id.id] = True
149         return result.keys()
150
151     def _get_default_company(self, cr, uid, context=None):
152         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
153         if not company_id:
154             raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
155         return company_id
156
157     def _get_default_section_id(self, cr, uid, context=None):
158         """ Gives default section by checking if present in the context """
159         section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
160         if not section_id:
161             section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
162         return section_id
163
164     def _resolve_section_id_from_context(self, cr, uid, context=None):
165         """ Returns ID of section based on the value of 'section_id'
166             context key, or None if it cannot be resolved to a single
167             Sales Team.
168         """
169         if context is None:
170             context = {}
171         if type(context.get('default_section_id')) in (int, long):
172             return context.get('default_section_id')
173         if isinstance(context.get('default_section_id'), basestring):
174             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
175             if len(section_ids) == 1:
176                 return int(section_ids[0][0])
177         return None
178
179     _columns = {
180         'name': fields.char('Order Reference', required=True, copy=False,
181             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
182         'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
183         'client_order_ref': fields.char('Customer Reference', copy=False),
184         'state': fields.selection([
185             ('draft', 'Draft Quotation'),
186             ('sent', 'Quotation Sent'),
187             ('cancel', 'Cancelled'),
188             ('waiting_date', 'Waiting Schedule'),
189             ('progress', 'Sales Order'),
190             ('manual', 'Sale to Invoice'),
191             ('shipping_except', 'Shipping Exception'),
192             ('invoice_except', 'Invoice Exception'),
193             ('done', 'Done'),
194             ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
195               \nThe exception status is automatically set when a cancel operation occurs \
196               in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
197                but waiting for the scheduler to run on the order date.", select=True),
198         'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
199         'validity_date': fields.date('Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
200         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
201         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
202         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
203         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
204         '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."),
205         '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."),
206         'order_policy': fields.selection([
207                 ('manual', 'On Demand'),
208             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
209             help="""This field controls how invoice and delivery operations are synchronized."""),
210         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
211         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
212         '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."),
213
214         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
215         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, 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)."),
216         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
217         'invoiced': fields.function(_invoiced, string='Paid',
218             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
219         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
220             fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
221         'note': fields.text('Terms and conditions'),
222
223         'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
224             store={
225                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
226                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
227             },
228             multi='sums', help="The amount without tax.", track_visibility='always'),
229         'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
230             store={
231                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
232                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
233             },
234             multi='sums', help="The tax amount."),
235         'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
236             store={
237                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
239             },
240             multi='sums', help="The total amount."),
241
242         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
243         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
244         'company_id': fields.many2one('res.company', 'Company'),
245         'section_id': fields.many2one('crm.case.section', 'Sales Team', change_default=True),
246         'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
247         'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
248     }
249
250     _defaults = {
251         'date_order': fields.datetime.now,
252         'order_policy': 'manual',
253         'company_id': _get_default_company,
254         'state': 'draft',
255         'user_id': lambda obj, cr, uid, context: uid,
256         'name': lambda obj, cr, uid, context: '/',
257         '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'],
258         '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'],
259         'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
260         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
261     }
262     _sql_constraints = [
263         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
264     ]
265     _order = 'date_order desc, id desc'
266
267     # Form filling
268     def unlink(self, cr, uid, ids, context=None):
269         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
270         unlink_ids = []
271         for s in sale_orders:
272             if s['state'] in ['draft', 'cancel']:
273                 unlink_ids.append(s['id'])
274             else:
275                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
276
277         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
278
279     def copy_quotation(self, cr, uid, ids, context=None):
280         id = self.copy(cr, uid, ids[0], context=context)
281         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
282         view_id = view_ref and view_ref[1] or False,
283         return {
284             'type': 'ir.actions.act_window',
285             'name': _('Sales Order'),
286             'res_model': 'sale.order',
287             'res_id': id,
288             'view_type': 'form',
289             'view_mode': 'form',
290             'view_id': view_id,
291             'target': 'current',
292             'nodestroy': True,
293         }
294
295     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
296         context = context or {}
297         if not pricelist_id:
298             return {}
299         value = {
300             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
301         }
302         if not order_lines or order_lines == [(6, 0, [])]:
303             return {'value': value}
304         warning = {
305             'title': _('Pricelist Warning!'),
306             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
307         }
308         return {'warning': warning, 'value': value}
309
310     def get_salenote(self, cr, uid, ids, partner_id, context=None):
311         context_lang = context.copy() 
312         if partner_id:
313             partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
314             context_lang.update({'lang': partner_lang})
315         return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
316
317     def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
318         r = {'value': {}}
319         if not fiscal_position:
320             if not company_id:
321                 company_id = self._get_default_company(cr, uid, context=context)
322             fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
323             if fiscal_position:
324                 r['value']['fiscal_position'] = fiscal_position
325         return r
326
327     def onchange_partner_id(self, cr, uid, ids, part, context=None):
328         if not part:
329             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
330
331         part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
332         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
333         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
334         payment_term = part.property_payment_term and part.property_payment_term.id or False
335         dedicated_salesman = part.user_id and part.user_id.id or uid
336         val = {
337             'partner_invoice_id': addr['invoice'],
338             'partner_shipping_id': addr['delivery'],
339             'payment_term': payment_term,
340             'user_id': dedicated_salesman,
341         }
342         delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False,  context=context)
343         val.update(delivery_onchange['value'])
344         if pricelist:
345             val['pricelist_id'] = pricelist
346         sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
347         if sale_note: val.update({'note': sale_note})  
348         return {'value': val}
349
350     def create(self, cr, uid, vals, context=None):
351         if context is None:
352             context = {}
353         if vals.get('name', '/') == '/':
354             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
355         if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']):
356             defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
357             if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
358                 delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
359                 defaults.update(delivery_onchange['value'])
360             vals = dict(defaults, **vals)
361         ctx = dict(context or {}, mail_create_nolog=True)
362         new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
363         self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
364         return new_id
365
366     def button_dummy(self, cr, uid, ids, context=None):
367         return True
368
369     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
370     #        can be removed after 6.1.
371     def _inv_get(self, cr, uid, order, context=None):
372         return {}
373
374     def _prepare_invoice(self, cr, uid, order, lines, context=None):
375         """Prepare the dict of values to create the new invoice for a
376            sales order. This method may be overridden to implement custom
377            invoice generation (making sure to call super() to establish
378            a clean extension chain).
379
380            :param browse_record order: sale.order record to invoice
381            :param list(int) line: list of invoice line IDs that must be
382                                   attached to the invoice
383            :return: dict of value to create() the invoice
384         """
385         if context is None:
386             context = {}
387         journal_ids = self.pool.get('account.journal').search(cr, uid,
388             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
389             limit=1)
390         if not journal_ids:
391             raise osv.except_osv(_('Error!'),
392                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
393         invoice_vals = {
394             'name': order.client_order_ref or '',
395             'origin': order.name,
396             'type': 'out_invoice',
397             'reference': order.client_order_ref or order.name,
398             'account_id': order.partner_id.property_account_receivable.id,
399             'partner_id': order.partner_invoice_id.id,
400             'journal_id': journal_ids[0],
401             'invoice_line': [(6, 0, lines)],
402             'currency_id': order.pricelist_id.currency_id.id,
403             'comment': order.note,
404             'payment_term': order.payment_term and order.payment_term.id or False,
405             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
406             'date_invoice': context.get('date_invoice', False),
407             'company_id': order.company_id.id,
408             'user_id': order.user_id and order.user_id.id or False,
409             'section_id' : order.section_id.id
410         }
411
412         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
413         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
414         return invoice_vals
415
416     def _make_invoice(self, cr, uid, order, lines, context=None):
417         inv_obj = self.pool.get('account.invoice')
418         obj_invoice_line = self.pool.get('account.invoice.line')
419         if context is None:
420             context = {}
421         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
422         from_line_invoice_ids = []
423         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
424             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
425                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
426                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
427         for preinv in order.invoice_ids:
428             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
429                 for preline in preinv.invoice_line:
430                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
431                     lines.append(inv_line_id)
432         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
433         inv_id = inv_obj.create(cr, uid, inv, context=context)
434         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
435         if data.get('value', False):
436             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
437         inv_obj.button_compute(cr, uid, [inv_id])
438         return inv_id
439
440     def print_quotation(self, cr, uid, ids, context=None):
441         '''
442         This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
443         '''
444         assert len(ids) == 1, 'This option should only be used for a single id at a time'
445         self.signal_workflow(cr, uid, ids, 'quotation_sent')
446         return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
447
448     def manual_invoice(self, cr, uid, ids, context=None):
449         """ create invoices for the given sales orders (ids), and open the form
450             view of one of the newly created invoices
451         """
452         mod_obj = self.pool.get('ir.model.data')
453         
454         # create invoices through the sales orders' workflow
455         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
456         self.signal_workflow(cr, uid, ids, 'manual_invoice')
457         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
458         # determine newly created invoices
459         new_inv_ids = list(inv_ids1 - inv_ids0)
460
461         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
462         res_id = res and res[1] or False,
463
464         return {
465             'name': _('Customer Invoices'),
466             'view_type': 'form',
467             'view_mode': 'form',
468             'view_id': [res_id],
469             'res_model': 'account.invoice',
470             'context': "{'type':'out_invoice'}",
471             'type': 'ir.actions.act_window',
472             'nodestroy': True,
473             'target': 'current',
474             'res_id': new_inv_ids and new_inv_ids[0] or False,
475         }
476
477     def action_view_invoice(self, cr, uid, ids, context=None):
478         '''
479         This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
480         '''
481         mod_obj = self.pool.get('ir.model.data')
482         act_obj = self.pool.get('ir.actions.act_window')
483
484         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
485         id = result and result[1] or False
486         result = act_obj.read(cr, uid, [id], context=context)[0]
487         #compute the number of invoices to display
488         inv_ids = []
489         for so in self.browse(cr, uid, ids, context=context):
490             inv_ids += [invoice.id for invoice in so.invoice_ids]
491         #choose the view_mode accordingly
492         if len(inv_ids)>1:
493             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
494         else:
495             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
496             result['views'] = [(res and res[1] or False, 'form')]
497             result['res_id'] = inv_ids and inv_ids[0] or False
498         return result
499
500     def test_no_product(self, cr, uid, order, context):
501         for line in order.order_line:
502             if line.product_id and (line.product_id.type<>'service'):
503                 return False
504         return True
505
506     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
507         if states is None:
508             states = ['confirmed', 'done', 'exception']
509         res = False
510         invoices = {}
511         invoice_ids = []
512         invoice = self.pool.get('account.invoice')
513         obj_sale_order_line = self.pool.get('sale.order.line')
514         partner_currency = {}
515         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
516         # last day of the last month as invoice date
517         if date_invoice:
518             context = dict(context or {}, date_invoice=date_invoice)
519         for o in self.browse(cr, uid, ids, context=context):
520             currency_id = o.pricelist_id.currency_id.id
521             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
522                 raise osv.except_osv(
523                     _('Error!'),
524                     _('You cannot group sales having different currencies for the same partner.'))
525
526             partner_currency[o.partner_id.id] = currency_id
527             lines = []
528             for line in o.order_line:
529                 if line.invoiced:
530                     continue
531                 elif (line.state in states):
532                     lines.append(line.id)
533             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
534             if created_lines:
535                 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
536         if not invoices:
537             for o in self.browse(cr, uid, ids, context=context):
538                 for i in o.invoice_ids:
539                     if i.state == 'draft':
540                         return i.id
541         for val in invoices.values():
542             if grouped:
543                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
544                 invoice_ref = ''
545                 origin_ref = ''
546                 for o, l in val:
547                     invoice_ref += (o.client_order_ref or o.name) + '|'
548                     origin_ref += (o.origin or o.name) + '|'
549                     self.write(cr, uid, [o.id], {'state': 'progress'})
550                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
551                     self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
552                 #remove last '|' in invoice_ref
553                 if len(invoice_ref) >= 1:
554                     invoice_ref = invoice_ref[:-1]
555                 if len(origin_ref) >= 1:
556                     origin_ref = origin_ref[:-1]
557                 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
558             else:
559                 for order, il in val:
560                     res = self._make_invoice(cr, uid, order, il, context=context)
561                     invoice_ids.append(res)
562                     self.write(cr, uid, [order.id], {'state': 'progress'})
563                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
564                     self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
565         return res
566
567     def action_invoice_cancel(self, cr, uid, ids, context=None):
568         self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
569         return True
570
571     def action_invoice_end(self, cr, uid, ids, context=None):
572         for this in self.browse(cr, uid, ids, context=context):
573             for line in this.order_line:
574                 if line.state == 'exception':
575                     line.write({'state': 'confirmed'})
576             if this.state == 'invoice_except':
577                 this.write({'state': 'progress'})
578         return True
579
580     def action_cancel(self, cr, uid, ids, context=None):
581         if context is None:
582             context = {}
583         sale_order_line_obj = self.pool.get('sale.order.line')
584         account_invoice_obj = self.pool.get('account.invoice')
585         for sale in self.browse(cr, uid, ids, context=context):
586             for inv in sale.invoice_ids:
587                 if inv.state not in ('draft', 'cancel'):
588                     raise osv.except_osv(
589                         _('Cannot cancel this sales order!'),
590                         _('First cancel all invoices attached to this sales order.'))
591                 inv.signal_workflow('invoice_cancel')
592             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
593                     {'state': 'cancel'})
594         self.write(cr, uid, ids, {'state': 'cancel'})
595         return True
596
597     def action_button_confirm(self, cr, uid, ids, context=None):
598         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
599         self.signal_workflow(cr, uid, ids, 'order_confirm')
600         return True
601         
602     def action_wait(self, cr, uid, ids, context=None):
603         context = context or {}
604         for o in self.browse(cr, uid, ids):
605             if not o.order_line:
606                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
607             noprod = self.test_no_product(cr, uid, o, context)
608             if (o.order_policy == 'manual') or noprod:
609                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
610             else:
611                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
612             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
613         return True
614
615     def action_quotation_send(self, cr, uid, ids, context=None):
616         '''
617         This function opens a window to compose an email, with the edi sale template message loaded by default
618         '''
619         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
620         ir_model_data = self.pool.get('ir.model.data')
621         try:
622             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
623         except ValueError:
624             template_id = False
625         try:
626             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
627         except ValueError:
628             compose_form_id = False 
629         ctx = dict()
630         ctx.update({
631             'default_model': 'sale.order',
632             'default_res_id': ids[0],
633             'default_use_template': bool(template_id),
634             'default_template_id': template_id,
635             'default_composition_mode': 'comment',
636             'mark_so_as_sent': True
637         })
638         return {
639             'type': 'ir.actions.act_window',
640             'view_type': 'form',
641             'view_mode': 'form',
642             'res_model': 'mail.compose.message',
643             'views': [(compose_form_id, 'form')],
644             'view_id': compose_form_id,
645             'target': 'new',
646             'context': ctx,
647         }
648
649     def action_done(self, cr, uid, ids, context=None):
650         for order in self.browse(cr, uid, ids, context=context):
651             self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
652         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
653
654     def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
655         date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
656         return {
657             'name': line.name,
658             'origin': order.name,
659             'date_planned': date_planned,
660             'product_id': line.product_id.id,
661             'product_qty': line.product_uom_qty,
662             'product_uom': line.product_uom.id,
663             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
664             'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
665             'company_id': order.company_id.id,
666             'group_id': group_id,
667             'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
668             'sale_line_id': line.id
669         }
670
671     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
672         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
673         return date_planned
674
675     def _prepare_procurement_group(self, cr, uid, order, context=None):
676         return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
677
678     def procurement_needed(self, cr, uid, ids, context=None):
679         #when sale is installed only, there is no need to create procurements, that's only
680         #further installed modules (sale_service, sale_stock) that will change this.
681         sale_line_obj = self.pool.get('sale.order.line')
682         res = []
683         for order in self.browse(cr, uid, ids, context=context):
684             res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
685         return any(res)
686
687     def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
688         for sale_order in self.browse(cr, uid, ids, context=context):
689             self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
690         return True
691
692     def action_ship_create(self, cr, uid, ids, context=None):
693         """Create the required procurements to supply sales order lines, also connecting
694         the procurements to appropriate stock moves in order to bring the goods to the
695         sales order's requested location.
696
697         :return: True
698         """
699         procurement_obj = self.pool.get('procurement.order')
700         sale_line_obj = self.pool.get('sale.order.line')
701         for order in self.browse(cr, uid, ids, context=context):
702             proc_ids = []
703             vals = self._prepare_procurement_group(cr, uid, order, context=context)
704             if not order.procurement_group_id:
705                 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
706                 order.write({'procurement_group_id': group_id}, context=context)
707
708             for line in order.order_line:
709                 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
710                 if line.procurement_ids:
711                     #first check them to see if they are in exception or not (one of the related moves is cancelled)
712                     procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
713                     line.refresh()
714                     #run again procurement that are in exception in order to trigger another move
715                     proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
716                 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
717                     if (line.state == 'done') or not line.product_id:
718                         continue
719                     vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
720                     proc_id = procurement_obj.create(cr, uid, vals, context=context)
721                     proc_ids.append(proc_id)
722             #Confirm procurement order such that rules will be applied on it
723             #note that the workflow normally ensure proc_ids isn't an empty list
724             procurement_obj.run(cr, uid, proc_ids, context=context)
725
726             #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
727             if order.state == 'shipping_except':
728                 val = {'state': 'progress', 'shipped': False}
729
730                 if (order.order_policy == 'manual'):
731                     for line in order.order_line:
732                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
733                             val['state'] = 'manual'
734                             break
735                 order.write(val)
736         return True
737
738
739
740     def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
741         '''Update taxes of order lines for each line where a product is defined
742
743         :param list ids: not used
744         :param int fiscal_position: sale order fiscal position
745         :param list order_lines: command list for one2many write method
746         '''
747         order_line = []
748         fiscal_obj = self.pool.get('account.fiscal.position')
749         product_obj = self.pool.get('product.product')
750         line_obj = self.pool.get('sale.order.line')
751
752         fpos = False
753         if fiscal_position:
754             fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
755         
756         for line in order_lines:
757             # create    (0, 0,  { fields })
758             # update    (1, ID, { fields })
759             if line[0] in [0, 1]:
760                 prod = None
761                 if line[2].get('product_id'):
762                     prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
763                 elif line[1]:
764                     prod =  line_obj.browse(cr, uid, line[1], context=context).product_id
765                 if prod and prod.taxes_id:
766                     line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
767                 order_line.append(line)
768
769             # link      (4, ID)
770             # link all  (6, 0, IDS)
771             elif line[0] in [4, 6]:
772                 line_ids = line[0] == 4 and [line[1]] or line[2]
773                 for line_id in line_ids:
774                     prod = line_obj.browse(cr, uid, line_id, context=context).product_id
775                     if prod and prod.taxes_id:
776                         order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
777                     else:
778                         order_line.append([4, line_id])
779             else:
780                 order_line.append(line)
781         return {'value': {'order_line': order_line}}
782
783     def test_procurements_done(self, cr, uid, ids, context=None):
784         for sale in self.browse(cr, uid, ids, context=context):
785             for line in sale.order_line:
786                 if not all([x.state == 'done' for x in line.procurement_ids]):
787                     return False
788         return True
789
790     def test_procurements_except(self, cr, uid, ids, context=None):
791         for sale in self.browse(cr, uid, ids, context=context):
792             for line in sale.order_line:
793                 if any([x.state == 'cancel' for x in line.procurement_ids]):
794                     return True
795         return False
796
797
798 # TODO add a field price_unit_uos
799 # - update it on change product and unit price
800 # - use it in report if there is a uos
801 class sale_order_line(osv.osv):
802
803     def need_procurement(self, cr, uid, ids, context=None):
804         #when sale is installed only, there is no need to create procurements, that's only
805         #further installed modules (sale_service, sale_stock) that will change this.
806         prod_obj = self.pool.get('product.product')
807         for line in self.browse(cr, uid, ids, context=context):
808             if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
809                 return True
810         return False
811
812     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
813         tax_obj = self.pool.get('account.tax')
814         cur_obj = self.pool.get('res.currency')
815         res = {}
816         if context is None:
817             context = {}
818         for line in self.browse(cr, uid, ids, context=context):
819             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
820             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
821             cur = line.order_id.pricelist_id.currency_id
822             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
823         return res
824
825     def _get_uom_id(self, cr, uid, *args):
826         try:
827             proxy = self.pool.get('ir.model.data')
828             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
829             return result[1]
830         except Exception, ex:
831             return False
832
833     def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
834         res = dict.fromkeys(ids, False)
835         for this in self.browse(cr, uid, ids, context=context):
836             res[this.id] = this.invoice_lines and \
837                 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) 
838         return res
839
840     def _order_lines_from_invoice(self, cr, uid, ids, context=None):
841         # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
842         cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
843                                                   sale_order_line sol ON (sol.order_id = rel.order_id)
844                                     WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
845         return [i[0] for i in cr.fetchall()]
846
847
848     _name = 'sale.order.line'
849     _description = 'Sales Order Line'
850     _columns = {
851         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
852         'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
853         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
854         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
855         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
856         'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
857             store={
858                 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
859                 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
860             }),
861         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
862         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
863         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
864         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
865         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
866         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
867         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
868         'product_uos': fields.many2one('product.uom', 'Product UoS'),
869         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
870         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
871         'state': fields.selection(
872                 [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
873                 'Status', required=True, readonly=True, copy=False,
874                 help='* The \'Draft\' status is set when the related sales order in draft status. \
875                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
876                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
877                     \n* The \'Done\' status is set when the sales order line has been picked. \
878                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
879         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
880         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
881         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
882         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
883         'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
884     }
885     _order = 'order_id desc, sequence, id'
886     _defaults = {
887         'product_uom' : _get_uom_id,
888         'discount': 0.0,
889         'product_uom_qty': 1,
890         'product_uos_qty': 1,
891         'sequence': 10,
892         'state': 'draft',
893         'price_unit': 0.0,
894         'delay': 0.0,
895     }
896
897
898
899     def _get_line_qty(self, cr, uid, line, context=None):
900         if line.product_uos:
901             return line.product_uos_qty or 0.0
902         return line.product_uom_qty
903
904     def _get_line_uom(self, cr, uid, line, context=None):
905         if line.product_uos:
906             return line.product_uos.id
907         return line.product_uom.id
908
909     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
910         """Prepare the dict of values to create the new invoice line for a
911            sales order line. This method may be overridden to implement custom
912            invoice generation (making sure to call super() to establish
913            a clean extension chain).
914
915            :param browse_record line: sale.order.line record to invoice
916            :param int account_id: optional ID of a G/L account to force
917                (this is used for returning products including service)
918            :return: dict of values to create() the invoice line
919         """
920         res = {}
921         if not line.invoiced:
922             if not account_id:
923                 if line.product_id:
924                     account_id = line.product_id.property_account_income.id
925                     if not account_id:
926                         account_id = line.product_id.categ_id.property_account_income_categ.id
927                     if not account_id:
928                         raise osv.except_osv(_('Error!'),
929                                 _('Please define income account for this product: "%s" (id:%d).') % \
930                                     (line.product_id.name, line.product_id.id,))
931                 else:
932                     prop = self.pool.get('ir.property').get(cr, uid,
933                             'property_account_income_categ', 'product.category',
934                             context=context)
935                     account_id = prop and prop.id or False
936             uosqty = self._get_line_qty(cr, uid, line, context=context)
937             uos_id = self._get_line_uom(cr, uid, line, context=context)
938             pu = 0.0
939             if uosqty:
940                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
941                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
942             fpos = line.order_id.fiscal_position or False
943             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
944             if not account_id:
945                 raise osv.except_osv(_('Error!'),
946                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
947             res = {
948                 'name': line.name,
949                 'sequence': line.sequence,
950                 'origin': line.order_id.name,
951                 'account_id': account_id,
952                 'price_unit': pu,
953                 'quantity': uosqty,
954                 'discount': line.discount,
955                 'uos_id': uos_id,
956                 'product_id': line.product_id.id or False,
957                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
958                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
959             }
960
961         return res
962
963     def invoice_line_create(self, cr, uid, ids, context=None):
964         if context is None:
965             context = {}
966
967         create_ids = []
968         sales = set()
969         for line in self.browse(cr, uid, ids, context=context):
970             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
971             if vals:
972                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
973                 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
974                 sales.add(line.order_id.id)
975                 create_ids.append(inv_id)
976         # Trigger workflow events
977         for sale_id in sales:
978             workflow.trg_write(uid, 'sale.order', sale_id, cr)
979         return create_ids
980
981     def button_cancel(self, cr, uid, ids, context=None):
982         for line in self.browse(cr, uid, ids, context=context):
983             if line.invoiced:
984                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
985         return self.write(cr, uid, ids, {'state': 'cancel'})
986
987     def button_confirm(self, cr, uid, ids, context=None):
988         return self.write(cr, uid, ids, {'state': 'confirmed'})
989
990     def button_done(self, cr, uid, ids, context=None):
991         res = self.write(cr, uid, ids, {'state': 'done'})
992         for line in self.browse(cr, uid, ids, context=context):
993             workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
994         return res
995
996     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
997         product_obj = self.pool.get('product.product')
998         if not product_id:
999             return {'value': {'product_uom': product_uos,
1000                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1001
1002         product = product_obj.browse(cr, uid, product_id)
1003         value = {
1004             'product_uom': product.uom_id.id,
1005         }
1006         # FIXME must depend on uos/uom of the product and not only of the coeff.
1007         try:
1008             value.update({
1009                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1010                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1011             })
1012         except ZeroDivisionError:
1013             pass
1014         return {'value': value}
1015
1016     def create(self, cr, uid, values, context=None):
1017         if values.get('order_id') and values.get('product_id') and  any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
1018             order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1019             defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1020                 qty=float(values.get('product_uom_qty', False)),
1021                 uom=values.get('product_uom', False),
1022                 qty_uos=float(values.get('product_uos_qty', False)),
1023                 uos=values.get('product_uos', False),
1024                 name=values.get('name', False),
1025                 partner_id=order['partner_id'][0],
1026                 date_order=order['date_order'],
1027                 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1028                 flag=False,  # Force name update
1029                 context=context
1030             )['value']
1031             if defaults.get('tax_id'):
1032                 defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
1033             values = dict(defaults, **values)
1034         return super(sale_order_line, self).create(cr, uid, values, context=context)
1035
1036     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1037             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1038             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1039         if context is None:
1040             context = {}
1041         Partner = self.pool['res.partner']
1042         ProductUom = self.pool['product.uom']
1043         Product = self.pool['product.product']
1044         ctx_product = dict(context)
1045         partner = False
1046         if partner_id:
1047             partner = Partner.browse(cr, uid, partner_id, context=context)
1048             ctx_product['lang'] = partner.lang
1049             ctx_product['partner_id'] = partner_id
1050         elif lang:
1051             ctx_product['lang'] = lang
1052
1053         if not product:
1054             return {'value': {'th_weight': 0,
1055                     'product_uos_qty': qty}, 'domain': {'product_uom': [],
1056                     'product_uos': []}}
1057         if not date_order:
1058             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1059
1060         result = {}
1061         product_obj = Product.browse(cr, uid, product, context=ctx_product)
1062
1063         uom2 = False
1064         if uom:
1065             uom2 = ProductUom.browse(cr, uid, uom, context=context)
1066             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1067                 uom = False
1068         if uos:
1069             if product_obj.uos_id:
1070                 uos2 = ProductUom.browse(cr, uid, uos, context=context)
1071                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1072                     uos = False
1073             else:
1074                 uos = False
1075
1076         fpos = False
1077         if not fiscal_position:
1078             fpos = partner and partner.property_account_position or False
1079         else:
1080             fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position)
1081         if update_tax:  # The quantity only have changed
1082             result['tax_id'] = self.pool['account.fiscal.position'].map_tax(cr, uid, fpos, product_obj.taxes_id)
1083
1084         if not flag:
1085             result['name'] = Product.name_get(cr, uid, [product_obj.id], context=ctx_product)[0][1]
1086             if product_obj.description_sale:
1087                 result['name'] += '\n'+product_obj.description_sale
1088         domain = {}
1089         if (not uom) and (not uos):
1090             result['product_uom'] = product_obj.uom_id.id
1091             if product_obj.uos_id:
1092                 result['product_uos'] = product_obj.uos_id.id
1093                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1094                 uos_category_id = product_obj.uos_id.category_id.id
1095             else:
1096                 result['product_uos'] = False
1097                 result['product_uos_qty'] = qty
1098                 uos_category_id = False
1099             result['th_weight'] = qty * product_obj.weight
1100             domain = {'product_uom':
1101                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1102                         'product_uos':
1103                         [('category_id', '=', uos_category_id)]}
1104         elif uos and not uom:  # only happens if uom is False
1105             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1106             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1107             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1108         elif uom:  # whether uos is set or not
1109             default_uom = product_obj.uom_id and product_obj.uom_id.id
1110             q = ProductUom._compute_qty(cr, uid, uom, qty, default_uom)
1111             if product_obj.uos_id:
1112                 result['product_uos'] = product_obj.uos_id.id
1113                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1114             else:
1115                 result['product_uos'] = False
1116                 result['product_uos_qty'] = qty
1117             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1118
1119         if not uom2:
1120             uom2 = product_obj.uom_id
1121
1122         if pricelist and partner_id:
1123             price = self.pool['product.pricelist'].price_get(cr, uid, [pricelist],
1124                     product, qty or 1.0, partner_id, {
1125                         'uom': uom or result.get('product_uom'),
1126                         'date': date_order,
1127                         })[pricelist]
1128         else:
1129             price = Product.price_get(cr, uid, [product], ptype='list_price', context=ctx_product)[product] or False
1130         result.update({'price_unit': price})
1131         return {'value': result, 'domain': domain}
1132
1133     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1134             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1135             lang=False, update_tax=True, date_order=False, context=None):
1136         context = context or {}
1137         lang = lang or ('lang' in context and context['lang'])
1138         if not uom:
1139             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1140         return self.product_id_change(cursor, user, ids, pricelist, product,
1141                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1142                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1143                 date_order=date_order, context=context)
1144
1145     def unlink(self, cr, uid, ids, context=None):
1146         if context is None:
1147             context = {}
1148         """Allows to delete sales order lines in draft,cancel states"""
1149         for rec in self.browse(cr, uid, ids, context=context):
1150             if rec.state not in ['draft', 'cancel']:
1151                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1152         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1153
1154
1155 class mail_compose_message(osv.Model):
1156     _inherit = 'mail.compose.message'
1157
1158     def send_mail(self, cr, uid, ids, context=None):
1159         context = context or {}
1160         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1161             context = dict(context, mail_post_autofollow=True)
1162             self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
1163         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1164
1165
1166 class account_invoice(osv.Model):
1167     _inherit = 'account.invoice'
1168
1169     def _get_default_section_id(self, cr, uid, context=None):
1170         """ Gives default section by checking if present in the context """
1171         section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1172         if not section_id:
1173             section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1174         return section_id
1175
1176     def _resolve_section_id_from_context(self, cr, uid, context=None):
1177         """ Returns ID of section based on the value of 'section_id'
1178             context key, or None if it cannot be resolved to a single
1179             Sales Team.
1180         """
1181         if context is None:
1182             context = {}
1183         if type(context.get('default_section_id')) in (int, long):
1184             return context.get('default_section_id')
1185         if isinstance(context.get('default_section_id'), basestring):
1186             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1187             if len(section_ids) == 1:
1188                 return int(section_ids[0][0])
1189         return None
1190
1191     _columns = {
1192         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1193     }
1194
1195     _defaults = {
1196         'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1197     }
1198
1199     def confirm_paid(self, cr, uid, ids, context=None):
1200         sale_order_obj = self.pool.get('sale.order')
1201         res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1202         so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1203         for so_id in so_ids:
1204             sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1205         return res
1206
1207     def unlink(self, cr, uid, ids, context=None):
1208         """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1209         invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1210         #if we can't cancel all invoices, do nothing
1211         if len(invoice_ids) == len(ids):
1212             #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1213             #it will trigger the workflow to put the sale order in an 'invoice exception' state
1214             for id in ids:
1215                 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1216         return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1217
1218
1219 class procurement_order(osv.osv):
1220     _inherit = 'procurement.order'
1221     _columns = {
1222         'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1223     }
1224
1225     def write(self, cr, uid, ids, vals, context=None):
1226         if isinstance(ids, (int, long)):
1227             ids = [ids]
1228         res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
1229         from openerp import workflow
1230         if vals.get('state') in ['done', 'cancel', 'exception']:
1231             for proc in self.browse(cr, uid, ids, context=context):
1232                 if proc.sale_line_id and proc.sale_line_id.order_id:
1233                     order_id = proc.sale_line_id.order_id.id
1234                     if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
1235                         workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
1236                     if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
1237                         workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
1238         return res
1239
1240 class product_product(osv.Model):
1241     _inherit = 'product.product'
1242
1243     def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1244         SaleOrderLine = self.pool['sale.order.line']
1245         return {
1246             product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1247             for product_id in ids
1248         }
1249
1250     _columns = {
1251         'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1252
1253     }
1254
1255 class product_template(osv.Model):
1256     _inherit = 'product.template'
1257
1258     def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1259         res = dict.fromkeys(ids, 0)
1260         for template in self.browse(cr, uid, ids, context=context):
1261             res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
1262         return res
1263     
1264     def action_view_sales(self, cr, uid, ids, context=None):
1265         act_obj = self.pool.get('ir.actions.act_window')
1266         mod_obj = self.pool.get('ir.model.data')
1267         product_ids = []
1268         for template in self.browse(cr, uid, ids, context=context):
1269             product_ids += [x.id for x in template.product_variant_ids]
1270         result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
1271         result = act_obj.read(cr, uid, [result], context=context)[0]
1272         result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
1273         return result
1274     
1275     
1276     _columns = {
1277         'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1278
1279     }
1280
1281 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: