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