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