[MERGE] Merge with lp:openobject-addons
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
25 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
30 import netsvc
31
32 class sale_shop(osv.osv):
33     _name = "sale.shop"
34     _description = "Sales Shop"
35     _columns = {
36         'name': fields.char('Shop Name', size=64, required=True),
37         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41         'company_id': fields.many2one('res.company', 'Company', required=False),
42     }
43     _defaults = {
44         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
45     }
46
47 sale_shop()
48
49 class sale_order(osv.osv):
50     _name = "sale.order"
51     _inherit = ['ir.needaction_mixin', 'mail.thread']
52     _description = "Sales Order"
53     
54
55     def copy(self, cr, uid, id, default=None, context=None):
56         if not default:
57             default = {}
58         default.update({
59             'state': 'draft',
60             'shipped': False,
61             'invoice_ids': [],
62             'picking_ids': [],
63             'date_confirm': False,
64             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
65         })
66         return super(sale_order, self).copy(cr, uid, id, default, context=context)
67
68     def _amount_line_tax(self, cr, uid, line, context=None):
69         val = 0.0
70         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']:
71             val += c.get('amount', 0.0)
72         return val
73
74     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75         cur_obj = self.pool.get('res.currency')
76         res = {}
77         for order in self.browse(cr, uid, ids, context=context):
78             res[order.id] = {
79                 'amount_untaxed': 0.0,
80                 'amount_tax': 0.0,
81                 'amount_total': 0.0,
82             }
83             val = val1 = 0.0
84             cur = order.pricelist_id.currency_id
85             for line in order.order_line:
86                 val1 += line.price_subtotal
87                 val += self._amount_line_tax(cr, uid, line, context=context)
88             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
91         return res
92
93     # This is False
94     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
95         if not ids:
96             return {}
97         res = {}
98         tmp = {}
99         for id in ids:
100             tmp[id] = {'picked': 0.0, 'total': 0.0}
101         cr.execute('''SELECT
102                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
103             FROM
104                 stock_move m
105             LEFT JOIN
106                 stock_picking p on (p.id=m.picking_id)
107             LEFT JOIN
108                 procurement_order mp on (mp.move_id=m.id)
109             WHERE
110                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
111
112         for item in cr.dictfetchall():
113             if item['move_state'] == 'cancel':
114                 continue
115
116             if item['picking_type'] == 'in':#this is a returned picking
117                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
120             else:
121                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
124
125         for order in self.browse(cr, uid, ids, context=context):
126             if order.shipped:
127                 res[order.id] = 100.0
128             else:
129                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130         return res
131
132     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
133         res = {}
134         for sale in self.browse(cursor, user, ids, context=context):
135             if sale.invoiced:
136                 res[sale.id] = 100.0
137                 continue
138             tot = 0.0
139             for invoice in sale.invoice_ids:
140                 if invoice.state not in ('draft', 'cancel'):
141                     tot += invoice.amount_untaxed
142             if tot:
143                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
144             else:
145                 res[sale.id] = 0.0
146         return res
147
148     def _invoiced(self, cursor, user, ids, name, arg, context=None):
149         res = {}
150         for sale in self.browse(cursor, user, ids, context=context):
151             res[sale.id] = True
152             invoice_existence = False
153             for invoice in sale.invoice_ids:
154                 if invoice.state!='cancel':
155                     invoice_existence = True
156                     if invoice.state != 'paid':
157                         res[sale.id] = False
158                         break
159             if not invoice_existence:
160                 res[sale.id] = False
161         return res
162
163     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
164         if not len(args):
165             return []
166         clause = ''
167         sale_clause = ''
168         no_invoiced = False
169         for arg in args:
170             if arg[1] == '=':
171                 if arg[2]:
172                     clause += 'AND inv.state = \'paid\''
173                 else:
174                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
175                     sale_clause = ',  sale_order AS sale '
176                     no_invoiced = True
177
178         cursor.execute('SELECT rel.order_id ' \
179                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180                 'WHERE rel.invoice_id = inv.id ' + clause)
181         res = cursor.fetchall()
182         if no_invoiced:
183             cursor.execute('SELECT sale.id ' \
184                     'FROM sale_order AS sale ' \
185                     'WHERE sale.id NOT IN ' \
186                         '(SELECT rel.order_id ' \
187                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188             res.extend(cursor.fetchall())
189         if not res:
190             return [('id', '=', 0)]
191         return [('id', 'in', [x[0] for x in res])]
192
193     def _get_order(self, cr, uid, ids, context=None):
194         result = {}
195         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196             result[line.order_id.id] = True
197         return result.keys()
198
199     _columns = {
200         'name': fields.char('Order Reference', size=64, required=True,
201             readonly=True, states={'draft': [('readonly', False)]}, select=True),
202         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
203         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204         'client_order_ref': fields.char('Customer Reference', size=64),
205         'state': fields.selection([
206             ('draft', 'Quotation'),
207             ('cancel', 'Cancelled'),
208             ('waiting_date', 'Waiting Schedule'),
209             ('manual', 'To Invoice'),
210             ('progress', 'In Progress'),
211             ('shipping_except', 'Shipping Exception'),
212             ('invoice_except', 'Invoice Exception'),
213             ('done', 'Done'),
214             ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
215         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
216         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
217         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
218         'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
219         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
220         'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
221         'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
222
223         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
224         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225             'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
226             help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
227         'order_policy': fields.selection([
228                 ('manual', 'On Demand'),
229                 ('picking', 'On Delivery Order'),
230                 ('prepaid', 'Before Delivery'),
231             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)]},
232             help="""This field controls how invoice and delivery operations are synchronized.
233   - With 'On Demand', the invoice is created manually when needed.
234   - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
235   - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
236         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
237         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
238
239         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
240         'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
241         'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
242         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
243         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
244         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
245         'invoiced': fields.function(_invoiced, string='Paid',
246             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
247         'note': fields.text('Notes'),
248
249         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
250             store = {
251                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
252                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
253             },
254             multi='sums', help="The amount without tax."),
255         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
256             store = {
257                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
258                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
259             },
260             multi='sums', help="The tax amount."),
261         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
262             store = {
263                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
264                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
265             },
266             multi='sums', help="The total amount."),
267
268         'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
269         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
270         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
271         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
272     }
273     _defaults = {
274         'picking_policy': 'direct',
275         'date_order': fields.date.context_today,
276         'order_policy': 'manual',
277         'state': 'draft',
278         'user_id': lambda obj, cr, uid, context: uid,
279         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
280         'invoice_quantity': 'order',
281         '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'],
282         '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'],
283     }
284     _sql_constraints = [
285         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
286     ]
287     _order = 'name desc'
288
289     # Form filling
290     def unlink(self, cr, uid, ids, context=None):
291         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
292         unlink_ids = []
293         for s in sale_orders:
294             if s['state'] in ['draft', 'cancel']:
295                 unlink_ids.append(s['id'])
296             else:
297                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
298
299         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
300
301     def onchange_shop_id(self, cr, uid, ids, shop_id):
302         v = {}
303         if shop_id:
304             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
305             v['project_id'] = shop.project_id.id
306             # Que faire si le client a une pricelist a lui ?
307             if shop.pricelist_id.id:
308                 v['pricelist_id'] = shop.pricelist_id.id
309         return {'value': v}
310
311     def action_cancel_draft(self, cr, uid, ids, context=None):
312         if not len(ids):
313             return False
314         cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
315         line_ids = map(lambda x: x[0], cr.fetchall())
316         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
317         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
318         wf_service = netsvc.LocalService("workflow")
319         for inv_id in ids:
320             # Deleting the existing instance of workflow for SO
321             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
322             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
323             self.action_cancel_draft_send_note(cr, uid, ids, context=context)
324         return True
325
326     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327         if (not pricelist_id) or (not order_lines):
328             return {}
329         warning = {
330             'title': _('Pricelist Warning!'),
331             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
332         }
333         return {'warning': warning}
334
335     def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
336         if not order_id:
337             return {}
338         val = {}
339         if not invoice_id:
340             val['partner_invoice_id'] = order_id
341         if not shipping_id:
342             val['partner_shipping_id'] = order_id
343         return {'value': val}
344
345     def onchange_partner_id(self, cr, uid, ids, part):
346         if not part:
347             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
348
349         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350         part = self.pool.get('res.partner').browse(cr, uid, part)
351         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352         payment_term = part.property_payment_term and part.property_payment_term.id or False
353         fiscal_position = part.property_account_position and part.property_account_position.id or False
354         dedicated_salesman = part.user_id and part.user_id.id or uid
355         val = {
356             'partner_invoice_id': addr['invoice'],
357             'partner_shipping_id': addr['delivery'],
358             'payment_term': payment_term,
359             'fiscal_position': fiscal_position,
360             'user_id': dedicated_salesman,
361         }
362         if pricelist:
363             val['pricelist_id'] = pricelist
364         return {'value': val}
365
366     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
367         if not policy:
368             return {}
369         inv_qty = 'order'
370         if policy == 'prepaid':
371             inv_qty = 'order'
372         elif policy == 'picking':
373             inv_qty = 'procurement'
374         return {'value': {'invoice_quantity': inv_qty}}
375
376     def write(self, cr, uid, ids, vals, context=None):
377         if vals.get('order_policy', False):
378             if vals['order_policy'] == 'prepaid':
379                 vals.update({'invoice_quantity': 'order'})
380             elif vals['order_policy'] == 'picking':
381                 vals.update({'invoice_quantity': 'procurement'})
382         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
383
384     def create(self, cr, uid, vals, context=None):
385         if vals.get('order_policy', False):
386             if vals['order_policy'] == 'prepaid':
387                 vals.update({'invoice_quantity': 'order'})
388             if vals['order_policy'] == 'picking':
389                 vals.update({'invoice_quantity': 'procurement'})
390         order =  super(sale_order, self).create(cr, uid, vals, context=context)
391         if order:
392             self.create_send_note(cr, uid, [order], context=context)
393         return order
394
395     def button_dummy(self, cr, uid, ids, context=None):
396         return True
397
398     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
399     #        can be removed after 6.1.
400     def _inv_get(self, cr, uid, order, context=None):
401         return {}
402
403     def _prepare_invoice(self, cr, uid, order, lines, context=None):
404         """Prepare the dict of values to create the new invoice for a
405            sale order. This method may be overridden to implement custom
406            invoice generation (making sure to call super() to establish
407            a clean extension chain).
408
409            :param browse_record order: sale.order record to invoice
410            :param list(int) line: list of invoice line IDs that must be
411                                   attached to the invoice
412            :return: dict of value to create() the invoice
413         """
414         if context is None:
415             context = {}
416         journal_ids = self.pool.get('account.journal').search(cr, uid,
417             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
418             limit=1)
419         if not journal_ids:
420             raise osv.except_osv(_('Error !'),
421                 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
422
423         invoice_vals = {
424             'name': order.client_order_ref or '',
425             'origin': order.name,
426             'type': 'out_invoice',
427             'reference': order.client_order_ref or order.name,
428             'account_id': order.partner_id.property_account_receivable.id,
429             'partner_id': order.partner_id.id,
430             'journal_id': journal_ids[0],
431             'invoice_line': [(6, 0, lines)],
432             'currency_id': order.pricelist_id.currency_id.id,
433             'comment': order.note,
434             'payment_term': order.payment_term and order.payment_term.id or False,
435             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
436             'date_invoice': context.get('date_invoice', False),
437             'company_id': order.company_id.id,
438             'user_id': order.user_id and order.user_id.id or False
439         }
440
441         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
442         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
443
444         return invoice_vals
445
446     def _make_invoice(self, cr, uid, order, lines, context=None):
447         inv_obj = self.pool.get('account.invoice')
448         obj_invoice_line = self.pool.get('account.invoice.line')
449         if context is None:
450             context = {}
451         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
452         from_line_invoice_ids = []
453         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
454             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
455                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
456                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
457         for preinv in order.invoice_ids:
458             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
459                 for preline in preinv.invoice_line:
460                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
461                     lines.append(inv_line_id)
462         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
463         inv_id = inv_obj.create(cr, uid, inv, context=context)
464         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
465         if data.get('value', False):
466             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
467         inv_obj.button_compute(cr, uid, [inv_id])
468         return inv_id
469
470     def manual_invoice(self, cr, uid, ids, context=None):
471         mod_obj = self.pool.get('ir.model.data')
472         wf_service = netsvc.LocalService("workflow")
473         inv_ids = set()
474         inv_ids1 = set()
475         for id in ids:
476             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
477                 inv_ids.add(record.id)
478         # inv_ids would have old invoices if any
479         for id in ids:
480             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
481             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
482                 inv_ids1.add(record.id)
483         inv_ids = list(inv_ids1.difference(inv_ids))
484
485         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
486         res_id = res and res[1] or False,
487
488         return {
489             'name': _('Customer Invoices'),
490             'view_type': 'form',
491             'view_mode': 'form',
492             'view_id': [res_id],
493             'res_model': 'account.invoice',
494             'context': "{'type':'out_invoice'}",
495             'type': 'ir.actions.act_window',
496             'nodestroy': True,
497             'target': 'current',
498             'res_id': inv_ids and inv_ids[0] or False,
499         }
500
501     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
502         res = False
503         invoices = {}
504         invoice_ids = []
505         picking_obj = self.pool.get('stock.picking')
506         invoice = self.pool.get('account.invoice')
507         obj_sale_order_line = self.pool.get('sale.order.line')
508         partner_currency = {}
509         if context is None:
510             context = {}
511         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
512         # last day of the last month as invoice date
513         if date_inv:
514             context['date_inv'] = date_inv
515         for o in self.browse(cr, uid, ids, context=context):
516             currency_id = o.pricelist_id.currency_id.id
517             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
518                 raise osv.except_osv(
519                     _('Error !'),
520                     _('You cannot group sales having different currencies for the same partner.'))
521
522             partner_currency[o.partner_id.id] = currency_id
523             lines = []
524             for line in o.order_line:
525                 if line.invoiced:
526                     continue
527                 elif (line.state in states):
528                     lines.append(line.id)
529             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
530             if created_lines:
531                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
532         if not invoices:
533             for o in self.browse(cr, uid, ids, context=context):
534                 for i in o.invoice_ids:
535                     if i.state == 'draft':
536                         return i.id
537         for val in invoices.values():
538             if grouped:
539                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
540                 invoice_ref = ''
541                 for o, l in val:
542                     invoice_ref += o.name + '|'
543                     self.write(cr, uid, [o.id], {'state': 'progress'})
544                     if o.order_policy == 'picking':
545                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
546                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
547                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
548             else:
549                 for order, il in val:
550                     res = self._make_invoice(cr, uid, order, il, context=context)
551                     invoice_ids.append(res)
552                     self.write(cr, uid, [order.id], {'state': 'progress'})
553                     if order.order_policy == 'picking':
554                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
555                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
556         if res:
557             self.invoice_send_note(cr, uid, ids, res, context)
558         return res
559
560     def action_invoice_cancel(self, cr, uid, ids, context=None):
561         if context is None:
562             context = {}
563         for sale in self.browse(cr, uid, ids, context=context):
564             for line in sale.order_line:
565                 #
566                 # Check if the line is invoiced (has asociated invoice
567                 # lines from non-cancelled invoices).
568                 #
569                 invoiced = False
570                 for iline in line.invoice_lines:
571                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
572                         invoiced = True
573                         break
574                 # Update the line (only when needed)
575                 if line.invoiced != invoiced:
576                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
577         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
578         return True
579
580     def action_invoice_end(self, cr, uid, ids, context=None):
581         for order in self.browse(cr, uid, ids, context=context):
582             #
583             # Update the sale order lines state (and invoiced flag).
584             #
585             for line in order.order_line:
586                 vals = {}
587                 #
588                 # Check if the line is invoiced (has asociated invoice
589                 # lines from non-cancelled invoices).
590                 #
591                 invoiced = False
592                 for iline in line.invoice_lines:
593                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
594                         invoiced = True
595                         break
596                 if line.invoiced != invoiced:
597                     vals['invoiced'] = invoiced
598                 # If the line was in exception state, now it gets confirmed.
599                 if line.state == 'exception':
600                     vals['state'] = 'confirmed'
601                 # Update the line (only when needed).
602                 if vals:
603                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
604             #
605             # Update the sales order state.
606             #
607             if order.state == 'invoice_except':
608                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
609         return True
610
611     def action_cancel(self, cr, uid, ids, context=None):
612         wf_service = netsvc.LocalService("workflow")
613         if context is None:
614             context = {}
615         sale_order_line_obj = self.pool.get('sale.order.line')
616         proc_obj = self.pool.get('procurement.order')
617         for sale in self.browse(cr, uid, ids, context=context):
618             for pick in sale.picking_ids:
619                 if pick.state not in ('draft', 'cancel'):
620                     raise osv.except_osv(
621                         _('Could not cancel sales order !'),
622                         _('You must first cancel all picking attached to this sales order.'))
623                 if pick.state == 'cancel':
624                     for mov in pick.move_lines:
625                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
626                         if proc_ids:
627                             for proc in proc_ids:
628                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
629             for r in self.read(cr, uid, ids, ['picking_ids']):
630                 for pick in r['picking_ids']:
631                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
632             for inv in sale.invoice_ids:
633                 if inv.state not in ('draft', 'cancel'):
634                     raise osv.except_osv(
635                         _('Could not cancel this sales order !'),
636                         _('You must first cancel all invoices attached to this sales order.'))
637             for r in self.read(cr, uid, ids, ['invoice_ids']):
638                 for inv in r['invoice_ids']:
639                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
640             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
641                     {'state': 'cancel'})
642             self.cancel_send_note(cr, uid, [sale.id], context=None)
643         self.write(cr, uid, ids, {'state': 'cancel'})
644         return True
645
646     def action_wait(self, cr, uid, ids, context=None):
647         for o in self.browse(cr, uid, ids):
648             if not o.order_line:
649                 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
650             if (o.order_policy == 'manual'):
651                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
652             else:
653                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
654             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
655             self.confirm_send_note(cr, uid, ids, context)
656         return True
657
658     def procurement_lines_get(self, cr, uid, ids, *args):
659         res = []
660         for order in self.browse(cr, uid, ids, context={}):
661             for line in order.order_line:
662                 if line.procurement_id:
663                     res.append(line.procurement_id.id)
664         return res
665
666     # if mode == 'finished':
667     #   returns True if all lines are done, False otherwise
668     # if mode == 'canceled':
669     #   returns True if there is at least one canceled line, False otherwise
670     def test_state(self, cr, uid, ids, mode, *args):
671         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
672         finished = True
673         canceled = False
674         notcanceled = False
675         write_done_ids = []
676         write_cancel_ids = []
677         for order in self.browse(cr, uid, ids, context={}):
678             for line in order.order_line:
679                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
680                     if line.state != 'done':
681                         write_done_ids.append(line.id)
682                 else:
683                     finished = False
684                 if line.procurement_id:
685                     if (line.procurement_id.state == 'cancel'):
686                         canceled = True
687                         if line.state != 'exception':
688                             write_cancel_ids.append(line.id)
689                     else:
690                         notcanceled = True
691         if write_done_ids:
692             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
693         if write_cancel_ids:
694             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
695
696         if mode == 'finished':
697             return finished
698         elif mode == 'canceled':
699             return canceled
700             if notcanceled:
701                 return False
702             return canceled
703
704     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
705         return {
706             'name': line.name,
707             'origin': order.name,
708             'date_planned': date_planned,
709             'product_id': line.product_id.id,
710             'product_qty': line.product_uom_qty,
711             'product_uom': line.product_uom.id,
712             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
713                     or line.product_uom_qty,
714             'product_uos': (line.product_uos and line.product_uos.id)\
715                     or line.product_uom.id,
716             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
717             'procure_method': line.type,
718             'move_id': move_id,
719             'company_id': order.company_id.id,
720             'note': line.notes
721         }
722
723     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
724         location_id = order.shop_id.warehouse_id.lot_stock_id.id
725         output_id = order.shop_id.warehouse_id.lot_output_id.id
726         return {
727             'name': line.name[:250],
728             'picking_id': picking_id,
729             'product_id': line.product_id.id,
730             'date': date_planned,
731             'date_expected': date_planned,
732             'product_qty': line.product_uom_qty,
733             'product_uom': line.product_uom.id,
734             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
735             'product_uos': (line.product_uos and line.product_uos.id)\
736                     or line.product_uom.id,
737             'product_packaging': line.product_packaging.id,
738             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
739             'location_id': location_id,
740             'location_dest_id': output_id,
741             'sale_line_id': line.id,
742             'tracking_id': False,
743             'state': 'draft',
744             #'state': 'waiting',
745             'note': line.notes,
746             'company_id': order.company_id.id,
747             'price_unit': line.product_id.standard_price or 0.0
748         }
749
750     def _prepare_order_picking(self, cr, uid, order, context=None):
751         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
752         return {
753             'name': pick_name,
754             'origin': order.name,
755             'date': order.date_order,
756             'type': 'out',
757             'state': 'auto',
758             'move_type': order.picking_policy,
759             'sale_id': order.id,
760             'partner_id': order.partner_shipping_id.id,
761             'note': order.note,
762             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
763             'company_id': order.company_id.id,
764         }
765
766     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
767         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
768         """
769         Define ship_recreate for process after shipping exception
770         param order: sale order to which the order lines belong
771         param line: sale order line records to procure
772         param move_id: the ID of stock move
773         param proc_id: the ID of procurement
774         """
775         move_obj = self.pool.get('stock.move')
776         if order.state == 'shipping_except':
777             for pick in order.picking_ids:
778                 for move in pick.move_lines:
779                     if move.state == 'cancel':
780                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
781                         if mov_ids:
782                             for mov in move_obj.browse(cr, uid, mov_ids):
783                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
784                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
785                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
786         return True
787
788     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
789         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
790         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
791         return date_planned
792
793     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
794         """Create the required procurements to supply sale order lines, also connecting
795         the procurements to appropriate stock moves in order to bring the goods to the
796         sale order's requested location.
797
798         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
799         a standard outgoing picking will be created to wrap the stock moves, as returned
800         by :meth:`~._prepare_order_picking`.
801
802         Modules that wish to customize the procurements or partition the stock moves over
803         multiple stock pickings may override this method and call ``super()`` with
804         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
805
806         :param browse_record order: sale order to which the order lines belong
807         :param list(browse_record) order_lines: sale order line records to procure
808         :param int picking_id: optional ID of a stock picking to which the created stock moves
809                                will be added. A new picking will be created if ommitted.
810         :return: True
811         """
812         move_obj = self.pool.get('stock.move')
813         picking_obj = self.pool.get('stock.picking')
814         procurement_obj = self.pool.get('procurement.order')
815         proc_ids = []
816
817         for line in order_lines:
818             if line.state == 'done':
819                 continue
820
821             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
822
823             if line.product_id:
824                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
825                     if not picking_id:
826                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
827                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
828                 else:
829                     # a service has no stock move
830                     move_id = False
831
832                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
833                 proc_ids.append(proc_id)
834                 line.write({'procurement_id': proc_id})
835                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
836
837         wf_service = netsvc.LocalService("workflow")
838         if picking_id:
839             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
840             self.delivery_send_note(cr, uid, [order.id], picking_id, context)
841
842
843         for proc_id in proc_ids:
844             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
845
846         val = {}
847         if order.state == 'shipping_except':
848             val['state'] = 'progress'
849             val['shipped'] = False
850
851             if (order.order_policy == 'manual'):
852                 for line in order.order_line:
853                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
854                         val['state'] = 'manual'
855                         break
856         order.write(val)
857         return True
858
859     def action_ship_create(self, cr, uid, ids, context=None):
860         for order in self.browse(cr, uid, ids, context=context):
861             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
862         return True
863
864     def action_ship_end(self, cr, uid, ids, context=None):
865         for order in self.browse(cr, uid, ids, context=context):
866             val = {'shipped': True}
867             if order.state == 'shipping_except':
868                 val['state'] = 'progress'
869                 if (order.order_policy == 'manual'):
870                     for line in order.order_line:
871                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
872                             val['state'] = 'manual'
873                             break
874             for line in order.order_line:
875                 towrite = []
876                 if line.state == 'exception':
877                     towrite.append(line.id)
878                 if towrite:
879                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
880             res = self.write(cr, uid, [order.id], val)
881             if res:
882                 self.delivery_end_send_note(cr, uid, [order.id], context=context)
883         return True
884
885     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
886         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
887         for inv in invs:
888             part = inv['partner_id'] and inv['partner_id'][0]
889             pr = inv['amount_untaxed'] or 0.0
890             partnertype = 'customer'
891             eventtype = 'sale'
892             event = {
893                 'name': 'Order: '+name,
894                 'som': False,
895                 'description': 'Order '+str(inv['id']),
896                 'document': '',
897                 'partner_id': part,
898                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
899                 'user_id': uid,
900                 'partner_type': partnertype,
901                 'probability': 1.0,
902                 'planned_revenue': pr,
903                 'planned_cost': 0.0,
904                 'type': eventtype
905             }
906             self.pool.get('res.partner.event').create(cr, uid, event)
907
908     def has_stockable_products(self, cr, uid, ids, *args):
909         for order in self.browse(cr, uid, ids):
910             for order_line in order.order_line:
911                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
912                     return True
913         return False
914     
915     # ------------------------------------------------
916     # OpenChatter methods and notifications
917     # ------------------------------------------------
918     
919     def get_needaction_user_ids(self, cr, uid, ids, context=None):
920         result = dict.fromkeys(ids, [])
921         for obj in self.browse(cr, uid, ids, context=context):
922             if (obj.state == 'manual' or obj.state == 'progress'):
923                 result[obj.id] = [obj.user_id.id]
924         return result
925  
926     def create_send_note(self, cr, uid, ids, context=None):
927         for obj in self.browse(cr, uid, ids, context=context):
928             self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
929             self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
930         
931     def confirm_send_note(self, cr, uid, ids, context=None):
932         for obj in self.browse(cr, uid, ids, context=context):
933             self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
934     
935     def cancel_send_note(self, cr, uid, ids, context=None):
936         for obj in self.browse(cr, uid, ids, context=context):
937             self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
938         
939     def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
940         for order in self.browse(cr, uid, ids, context=context):
941             for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
942                 # convert datetime field to a datetime, using server format, then
943                 # convert it to the user TZ and re-render it with %Z to add the timezone
944                 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
945                 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
946                 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
947     
948     def delivery_end_send_note(self, cr, uid, ids, context=None):
949         self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
950      
951     def invoice_paid_send_note(self, cr, uid, ids, context=None):
952         self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
953         
954     def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
955         for order in self.browse(cr, uid, ids, context=context):
956             for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
957                 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
958     
959     def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
960         return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
961             
962         
963 sale_order()
964
965 # TODO add a field price_unit_uos
966 # - update it on change product and unit price
967 # - use it in report if there is a uos
968 class sale_order_line(osv.osv):
969
970     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
971         tax_obj = self.pool.get('account.tax')
972         cur_obj = self.pool.get('res.currency')
973         res = {}
974         if context is None:
975             context = {}
976         for line in self.browse(cr, uid, ids, context=context):
977             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
978             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
979             cur = line.order_id.pricelist_id.currency_id
980             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
981         return res
982
983     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
984         res = {}
985         for line in self.browse(cr, uid, ids, context=context):
986             try:
987                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
988             except:
989                 res[line.id] = 1
990         return res
991
992     def _get_uom_id(self, cr, uid, *args):
993         try:
994             proxy = self.pool.get('ir.model.data')
995             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
996             return result[1]
997         except Exception, ex:
998             return False
999
1000     _name = 'sale.order.line'
1001     _description = 'Sales Order Line'
1002     _columns = {
1003         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1004         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1005         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1006         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
1007         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1008         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1009         'invoiced': fields.boolean('Invoiced', readonly=True),
1010         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1011         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1012         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1013         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1014         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1015             help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
1016         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1017         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1018         'product_uom_qty': fields.float('Quantity (Unit of Measure)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1019         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1020         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1021         'product_uos': fields.many2one('product.uom', 'Product UoS'),
1022         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1023         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1024         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1025         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1026         'notes': fields.text('Notes'),
1027         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1028         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
1029                 help='* The \'Draft\' state is set when the related sales order in draft state. \
1030                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1031                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
1032                     \n* The \'Done\' state is set when the sales order line has been picked. \
1033                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1034         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1035         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
1036         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1037     }
1038     _order = 'sequence, id'
1039     _defaults = {
1040         'product_uom' : _get_uom_id,
1041         'discount': 0.0,
1042         'delay': 0.0,
1043         'product_uom_qty': 1,
1044         'product_uos_qty': 1,
1045         'sequence': 10,
1046         'invoiced': 0,
1047         'state': 'draft',
1048         'type': 'make_to_stock',
1049         'product_packaging': False,
1050         'price_unit': 0.0,
1051     }
1052
1053     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1054         """Prepare the dict of values to create the new invoice line for a
1055            sale order line. This method may be overridden to implement custom
1056            invoice generation (making sure to call super() to establish
1057            a clean extension chain).
1058
1059            :param browse_record line: sale.order.line record to invoice
1060            :param int account_id: optional ID of a G/L account to force
1061                (this is used for returning products including service)
1062            :return: dict of values to create() the invoice line
1063         """
1064
1065         def _get_line_qty(line):
1066             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1067                 if line.product_uos:
1068                     return line.product_uos_qty or 0.0
1069                 return line.product_uom_qty
1070             else:
1071                 return self.pool.get('procurement.order').quantity_get(cr, uid,
1072                         line.procurement_id.id, context=context)
1073
1074         def _get_line_uom(line):
1075             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1076                 if line.product_uos:
1077                     return line.product_uos.id
1078                 return line.product_uom.id
1079             else:
1080                 return self.pool.get('procurement.order').uom_get(cr, uid,
1081                         line.procurement_id.id, context=context)
1082
1083         if not line.invoiced:
1084             if not account_id:
1085                 if line.product_id:
1086                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1087                     if not account_id:
1088                         account_id = line.product_id.categ_id.property_account_income_categ.id
1089                     if not account_id:
1090                         raise osv.except_osv(_('Error !'),
1091                                 _('There is no income account defined for this product: "%s" (id:%d)') % \
1092                                     (line.product_id.name, line.product_id.id,))
1093                 else:
1094                     prop = self.pool.get('ir.property').get(cr, uid,
1095                             'property_account_income_categ', 'product.category',
1096                             context=context)
1097                     account_id = prop and prop.id or False
1098             uosqty = _get_line_qty(line)
1099             uos_id = _get_line_uom(line)
1100             pu = 0.0
1101             if uosqty:
1102                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1103                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1104             fpos = line.order_id.fiscal_position or False
1105             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1106             if not account_id:
1107                 raise osv.except_osv(_('Error !'),
1108                             _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1109             return {
1110                 'name': line.name,
1111                 'origin': line.order_id.name,
1112                 'account_id': account_id,
1113                 'price_unit': pu,
1114                 'quantity': uosqty,
1115                 'discount': line.discount,
1116                 'uos_id': uos_id,
1117                 'product_id': line.product_id.id or False,
1118                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1119                 'note': line.notes,
1120                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1121             }
1122
1123         return False
1124
1125     def invoice_line_create(self, cr, uid, ids, context=None):
1126         if context is None:
1127             context = {}
1128
1129         create_ids = []
1130         sales = set()
1131         for line in self.browse(cr, uid, ids, context=context):
1132             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1133             if vals:
1134                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1135                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1136                 self.write(cr, uid, [line.id], {'invoiced': True})
1137                 sales.add(line.order_id.id)
1138                 create_ids.append(inv_id)
1139         # Trigger workflow events
1140         wf_service = netsvc.LocalService("workflow")
1141         for sale_id in sales:
1142             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1143         return create_ids
1144
1145     def button_cancel(self, cr, uid, ids, context=None):
1146         for line in self.browse(cr, uid, ids, context=context):
1147             if line.invoiced:
1148                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1149             for move_line in line.move_ids:
1150                 if move_line.state != 'cancel':
1151                     raise osv.except_osv(
1152                             _('Could not cancel sales order line!'),
1153                             _('You must first cancel stock moves attached to this sales order line.'))
1154         return self.write(cr, uid, ids, {'state': 'cancel'})
1155
1156     def button_confirm(self, cr, uid, ids, context=None):
1157         return self.write(cr, uid, ids, {'state': 'confirmed'})
1158
1159     def button_done(self, cr, uid, ids, context=None):
1160         wf_service = netsvc.LocalService("workflow")
1161         res = self.write(cr, uid, ids, {'state': 'done'})
1162         for line in self.browse(cr, uid, ids, context=context):
1163             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1164         return res
1165
1166     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1167         product_obj = self.pool.get('product.product')
1168         if not product_id:
1169             return {'value': {'product_uom': product_uos,
1170                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1171
1172         product = product_obj.browse(cr, uid, product_id)
1173         value = {
1174             'product_uom': product.uom_id.id,
1175         }
1176         # FIXME must depend on uos/uom of the product and not only of the coeff.
1177         try:
1178             value.update({
1179                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1180                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1181             })
1182         except ZeroDivisionError:
1183             pass
1184         return {'value': value}
1185
1186     def copy_data(self, cr, uid, id, default=None, context=None):
1187         if not default:
1188             default = {}
1189         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1190         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1191
1192     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1193                                    partner_id=False, packaging=False, flag=False, context=None):
1194         if not product:
1195             return {'value': {'product_packaging': False}}
1196         product_obj = self.pool.get('product.product')
1197         product_uom_obj = self.pool.get('product.uom')
1198         pack_obj = self.pool.get('product.packaging')
1199         warning = {}
1200         result = {}
1201         warning_msgs = ''
1202         if flag:
1203             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1204                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1205                     packaging=packaging, flag=False, context=context)
1206             warning_msgs = res.get('warning') and res['warning']['message']
1207
1208         products = product_obj.browse(cr, uid, product, context=context)
1209         if not products.packaging:
1210             packaging = result['product_packaging'] = False
1211         elif not packaging and products.packaging and not flag:
1212             packaging = products.packaging[0].id
1213             result['product_packaging'] = packaging
1214
1215         if packaging:
1216             default_uom = products.uom_id and products.uom_id.id
1217             pack = pack_obj.browse(cr, uid, packaging, context=context)
1218             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1219 #            qty = qty - qty % q + q
1220             if qty and (q and not (qty % q) == 0):
1221                 ean = pack.ean or _('(n/a)')
1222                 qty_pack = pack.qty
1223                 type_ul = pack.ul
1224                 if not warning_msgs:
1225                     warn_msg = _("You selected a quantity of %d Units.\n"
1226                                 "But it's not compatible with the selected packaging.\n"
1227                                 "Here is a proposition of quantities according to the packaging:\n"
1228                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1229                                     (qty, ean, qty_pack, type_ul.name)
1230                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1231                 warning = {
1232                        'title': _('Configuration Error !'),
1233                        'message': warning_msgs
1234                 }
1235             result['product_uom_qty'] = qty
1236
1237         return {'value': result, 'warning': warning}
1238
1239     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1240             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1241             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1242         context = context or {}
1243         lang = lang or context.get('lang',False)
1244         if not  partner_id:
1245             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1246         warning = {}
1247         product_uom_obj = self.pool.get('product.uom')
1248         partner_obj = self.pool.get('res.partner')
1249         product_obj = self.pool.get('product.product')
1250         context = {'lang': lang, 'partner_id': partner_id}
1251         if partner_id:
1252             lang = partner_obj.browse(cr, uid, partner_id).lang
1253         context_partner = {'lang': lang, 'partner_id': partner_id}
1254
1255         if not product:
1256             return {'value': {'th_weight': 0, 'product_packaging': False,
1257                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1258                    'product_uos': []}}
1259         if not date_order:
1260             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1261
1262         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1263         result = res.get('value', {})
1264         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1265         product_obj = product_obj.browse(cr, uid, product, context=context)
1266
1267         uom2 = False
1268         if uom:
1269             uom2 = product_uom_obj.browse(cr, uid, uom)
1270             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1271                 uom = False
1272         if uos:
1273             if product_obj.uos_id:
1274                 uos2 = product_uom_obj.browse(cr, uid, uos)
1275                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1276                     uos = False
1277             else:
1278                 uos = False
1279         if product_obj.description_sale:
1280             result['notes'] = product_obj.description_sale
1281         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1282         if update_tax: #The quantity only have changed
1283             result['delay'] = (product_obj.sale_delay or 0.0)
1284             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1285             result.update({'type': product_obj.procure_method})
1286
1287         if not flag:
1288             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1289         domain = {}
1290         if (not uom) and (not uos):
1291             result['product_uom'] = product_obj.uom_id.id
1292             if product_obj.uos_id:
1293                 result['product_uos'] = product_obj.uos_id.id
1294                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1295                 uos_category_id = product_obj.uos_id.category_id.id
1296             else:
1297                 result['product_uos'] = False
1298                 result['product_uos_qty'] = qty
1299                 uos_category_id = False
1300             result['th_weight'] = qty * product_obj.weight
1301             domain = {'product_uom':
1302                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1303                         'product_uos':
1304                         [('category_id', '=', uos_category_id)]}
1305
1306         elif uos and not uom: # only happens if uom is False
1307             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1308             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1309             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1310         elif uom: # whether uos is set or not
1311             default_uom = product_obj.uom_id and product_obj.uom_id.id
1312             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1313             if product_obj.uos_id:
1314                 result['product_uos'] = product_obj.uos_id.id
1315                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1316             else:
1317                 result['product_uos'] = False
1318                 result['product_uos_qty'] = qty
1319             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1320
1321         if not uom2:
1322             uom2 = product_obj.uom_id
1323         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1324         if (product_obj.type=='product') and int(compare_qty) == -1 \
1325           and (product_obj.procure_method=='make_to_stock'):
1326             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1327                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1328                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1329                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1330             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1331         # get unit price
1332
1333         if not pricelist:
1334             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1335                     'Please set one before choosing a product.')
1336             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1337         else:
1338             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1339                     product, qty or 1.0, partner_id, {
1340                         'uom': uom or result.get('product_uom'),
1341                         'date': date_order,
1342                         })[pricelist]
1343             if price is False:
1344                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1345                         "You have to change either the product, the quantity or the pricelist.")
1346
1347                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1348             else:
1349                 result.update({'price_unit': price})
1350         if warning_msgs:
1351             warning = {
1352                        'title': _('Configuration Error !'),
1353                        'message' : warning_msgs
1354                     }
1355         return {'value': result, 'domain': domain, 'warning': warning}
1356
1357     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1358             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1359             lang=False, update_tax=True, date_order=False, context=None):
1360         context = context or {}
1361         lang = lang or ('lang' in context and context['lang'])
1362         res = self.product_id_change(cursor, user, ids, pricelist, product,
1363                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1364                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1365                 date_order=date_order, context=context)
1366         if 'product_uom' in res['value']:
1367             del res['value']['product_uom']
1368         if not uom:
1369             res['value']['price_unit'] = 0.0
1370         return res
1371
1372     def unlink(self, cr, uid, ids, context=None):
1373         if context is None:
1374             context = {}
1375         """Allows to delete sales order lines in draft,cancel states"""
1376         for rec in self.browse(cr, uid, ids, context=context):
1377             if rec.state not in ['draft', 'cancel']:
1378                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1379         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1380
1381 sale_order_line()
1382
1383 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: