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