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