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