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