Fix unit used in SO
[odoo/odoo.git] / addons / sale / sale.py
1 ##############################################################################
2 #
3 # Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
4 #
5 # $Id: sale.py 1005 2005-07-25 08:41:42Z nicoe $
6 #
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
12 # Service Company
13 #
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
27 #
28 ##############################################################################
29
30 import time
31 import netsvc
32 from osv import fields, osv
33 import ir
34 from mx import DateTime
35 from tools import config
36
37 class sale_shop(osv.osv):
38         _name = "sale.shop"
39         _description = "Sale Shop"
40         _columns = {
41                 'name': fields.char('Shop name',size=64, required=True),
42                 'payment_default_id': fields.many2one('account.payment.term','Default Payment Term',required=True),
43                 'payment_account_id': fields.many2many('account.account','sale_shop_account','shop_id','account_id','Payment accounts'),
44                 'warehouse_id': fields.many2one('stock.warehouse','Warehouse'),
45                 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
46                 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
47         }
48 sale_shop()
49
50 def _incoterm_get(self, cr, uid, context={}):
51         cr.execute('select code, code||\', \'||name from stock_incoterms where active')
52         return cr.fetchall()
53
54 class sale_order(osv.osv):
55         _name = "sale.order"
56         _description = "Sale Order"
57         def copy(self, cr, uid, id, default=None,context={}):
58                 if not default:
59                         default = {}
60                 default.update({
61                         'state':'draft',
62                         'shipped':False,
63                         'invoiced':False,
64                         'invoice_ids':[],
65                         'picking_ids':[],
66                         'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
67                 })
68                 return super(sale_order, self).copy(cr, uid, id, default, context)
69
70         def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
71                 res = {}
72                 cur_obj=self.pool.get('res.currency')
73                 for sale in self.browse(cr, uid, ids):
74                         res[sale.id] = 0.0
75                         for line in sale.order_line:
76                                 res[sale.id] += line.price_subtotal
77                         cur = sale.pricelist_id.currency_id
78                         res[sale.id] = cur_obj.round(cr, uid, cur, res[sale.id])
79                 return res
80
81         def _amount_tax(self, cr, uid, ids, field_name, arg, context):
82                 res = {}
83                 cur_obj=self.pool.get('res.currency')
84                 for order in self.browse(cr, uid, ids):
85                         val = 0.0
86                         cur=order.pricelist_id.currency_id
87                         for line in order.order_line:
88                                 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, order.partner_invoice_id.id, line.product_id, order.partner_id):
89                                         val+= cur_obj.round(cr, uid, cur, c['amount'])
90                         res[order.id]=cur_obj.round(cr, uid, cur, val)
91                 return res
92
93         def _amount_total(self, cr, uid, ids, field_name, arg, context):
94                 res = {}
95                 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context) 
96                 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
97                 cur_obj=self.pool.get('res.currency')
98                 for id in ids:
99                         order=self.browse(cr, uid, [id])[0]
100                         cur=order.pricelist_id.currency_id
101                         res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
102                 return res
103
104         _columns = {
105                 'name': fields.char('Order Description', size=64, required=True, select=True),
106                 'shop_id':fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft':[('readonly',False)]}),
107                 'origin': fields.char('Origin', size=64),
108                 'client_order_ref': fields.char('Partner Ref.',size=64),
109
110                 'state': fields.selection([
111                         ('draft','Quotation'),
112                         ('waiting_date','Waiting Schedule'),
113                         ('manual','Manual in progress'),
114                         ('progress','In progress'),
115                         ('shipping_except','Shipping Exception'),
116                         ('invoice_except','Invoice Exception'),
117                         ('done','Done'),
118                         ('cancel','Cancel')
119                         ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the packing list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to be on the date 'Date Ordered'.", select=True),
120                 'date_order':fields.date('Date Ordered', required=True, readonly=True, states={'draft':[('readonly',False)]}),
121
122                 'user_id':fields.many2one('res.users', 'Salesman', states={'draft':[('readonly',False)]}, select=True),
123                 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, select=True),
124                 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
125                 'partner_order_id':fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft':[('readonly',False)]}, help="The name and address of the contact that requested the order or quotation."),
126                 'partner_shipping_id':fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
127
128                 'incoterm': fields.selection(_incoterm_get, 'Incoterm',size=3),
129                 'picking_policy': fields.selection([('direct','Direct Delivery'),('one','All at once')], 'Packing Policy', required=True ),
130                 'order_policy': fields.selection([
131                         ('prepaid','Invoice before delivery'),
132                         ('manual','Shipping & Manual Invoice'),
133                         ('postpaid','Automatic Invoice after delivery'),
134                         ('picking','Invoice from the packings'),
135                 ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]},
136                                         help="""The Shipping Policy is used to synchronise invoice and delivery operations.
137   - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
138   - 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.
139   - The 'Invoice after delivery' choice will generate the draft invoice after the packing list have been finished.
140   - The 'Invoice from the packings' choice is used to create an invoice during the packing process."""),
141                 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}),
142                 'project_id':fields.many2one('account.analytic.account', 'Analytic account', readonly=True, states={'draft':[('readonly', False)]}),
143
144                 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}),
145                 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoice', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
146                 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Packing List', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
147
148                 'shipped':fields.boolean('Picked', readonly=True),
149                 'invoiced':fields.boolean('Paid', readonly=True),
150
151                 'note': fields.text('Notes'),
152
153                 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
154                 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
155                 'amount_total': fields.function(_amount_total, method=True, string='Total'),
156                 '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."),
157         }
158         _defaults = {
159                 'picking_policy': lambda *a: 'direct',
160                 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
161                 'order_policy': lambda *a: 'manual',
162                 'state': lambda *a: 'draft',
163                 'user_id': lambda obj, cr, uid, context: uid,
164                 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
165                 'invoice_quantity': lambda *a: 'order',
166                 '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'],
167                 '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'],
168                 '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'],
169                 '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,
170         }
171         _order = 'name desc'
172
173         # Form filling
174         def onchange_shop_id(self, cr, uid, ids, shop_id):
175                 v={}
176                 if shop_id:
177                         shop=self.pool.get('sale.shop').browse(cr,uid,shop_id)
178                         v['project_id']=shop.project_id.id
179                         # Que faire si le client a une pricelist a lui ?
180                         if shop.pricelist_id.id:
181                                 v['pricelist_id']=shop.pricelist_id.id
182                         #v['payment_default_id']=shop.payment_default_id.id
183                 return {'value':v}
184
185         def action_cancel_draft(self, cr, uid, ids, *args):
186                 if not len(ids):
187                         return False
188                 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
189                 line_ids = map(lambda x: x[0], cr.fetchall())
190                 self.write(cr, uid, ids, {'state':'draft', 'invoice_ids':[], 'shipped':0, 'invoiced':0})
191                 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced':False, 'state':'draft', 'invoice_lines':[(6,0,[])]})
192                 wf_service = netsvc.LocalService("workflow")
193                 for inv_id in ids:
194                         wf_service.trg_create(uid, 'sale.order', inv_id, cr)
195                 return True
196
197         def onchange_partner_id(self, cr, uid, ids, part):
198                 if not part:
199                         return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False}}
200                 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact'])
201                 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
202                 return {'value':{'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'pricelist_id': pricelist}}
203
204         def button_dummy(self, cr, uid, ids, context={}):
205                 return True
206
207 #FIXME: the method should return the list of invoices created (invoice_ids) 
208 # and not the id of the last invoice created (res). The problem is that we 
209 # cannot change it directly since the method is called by the sale order
210 # workflow and I suppose it expects a single id...
211         def _inv_get(self, cr, uid, order, context={}):
212                 return {}
213
214         def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done']):
215                 res = False
216                 invoices = {}
217                 invoice_ids = []
218
219                 def make_invoice(order, lines):
220                         a = order.partner_id.property_account_receivable.id
221                         if order.partner_id and order.partner_id.property_payment_term.id:
222                                 pay_term = order.partner_id.property_payment_term.id
223                         else:
224                                 pay_term = False
225                         for preinv in order.invoice_ids:
226                                 if preinv.state in ('open','paid','proforma'):
227                                         for preline in preinv.invoice_line:
228                                                 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id':False, 'price_unit':-preline.price_unit})
229                                                 lines.append(inv_line_id)
230                         inv = {
231                                 'name': order.name,
232                                 'origin': order.name,
233                                 'type': 'out_invoice',
234                                 'reference': "P%dSO%d"%(order.partner_id.id,order.id),
235                                 'account_id': a,
236                                 'partner_id': order.partner_id.id,
237                                 'address_invoice_id': order.partner_invoice_id.id,
238                                 'address_contact_id': order.partner_invoice_id.id,
239                                 'invoice_line': [(6,0,lines)],
240                                 'currency_id' : order.pricelist_id.currency_id.id,
241                                 'comment': order.note,
242                                 'payment_term': pay_term,
243                         }
244                         inv.update(self._inv_get(cr, uid, order))
245                         inv_obj = self.pool.get('account.invoice')
246                         inv_id = inv_obj.create(cr, uid, inv)
247                         inv_obj.button_compute(cr, uid, [inv_id])
248                         return inv_id
249
250                 for o in self.browse(cr,uid,ids):
251                         lines = []
252                         for line in o.order_line:
253                                 if (line.state in states) and not line.invoiced:
254                                         lines.append(line.id)
255                         created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
256                         if created_lines:
257                                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
258
259                 if not invoices:
260                         for o in self.browse(cr, uid, ids):
261                                 for i in o.invoice_ids:
262                                         if i.state == 'draft':
263                                                 return i.id
264
265                 for val in invoices.values():
266                         if grouped:
267                                 res = make_invoice(val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []))
268                                 for o,l in val:
269                                         self.write(cr, uid, [o.id], {'state' : 'progress'})
270                                         cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
271                         else:
272                                 for order, il in val:
273                                         res = make_invoice(order, il)
274                                         invoice_ids.append(res)
275                                         self.write(cr, uid, [order.id], {'state' : 'progress'})
276                                         cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (order.id, res))
277                 return res
278
279         def action_invoice_cancel(self, cr, uid, ids, context={}):
280                 for sale in self.browse(cr, uid, ids):
281                         for line in sale.order_line:
282                                 invoiced=False
283                                 for iline in line.invoice_lines:
284                                         if iline.invoice_id and iline.invoice_id.state == 'cancel':
285                                                 continue
286                                         else:
287                                                 invoiced=True
288                                 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
289                 self.write(cr, uid, ids, {'state':'invoice_except', 'invoice_id':False})
290                 return True
291
292
293         def action_cancel(self, cr, uid, ids, context={}):
294                 ok = True
295                 sale_order_line_obj = self.pool.get('sale.order.line')
296                 for sale in self.browse(cr, uid, ids):
297                         for pick in sale.picking_ids:
298                                 if pick.state not in ('draft','cancel'):
299                                         raise osv.except_osv(
300                                                 'Could not cancel sale order !',
301                                                 'You must first cancel all packings attached to this sale order.')
302                         for r in self.read(cr,uid,ids,['picking_ids']):
303                                 for pick in r['picking_ids']:
304                                         wf_service = netsvc.LocalService("workflow")
305                                         wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
306                         for inv in sale.invoice_ids:
307                                 if inv.state not in ('draft','cancel'):
308                                         raise osv.except_osv(
309                                                 'Could not cancel this sale order !',
310                                                 'You must first cancel all invoices attached to this sale order.')
311                         for r in self.read(cr,uid,ids,['invoice_ids']):
312                                 for inv in r['invoice_ids']:
313                                         wf_service = netsvc.LocalService("workflow")
314                                         wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
315                         sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
316                                         {'state': 'cancel'})
317                 self.write(cr,uid,ids,{'state':'cancel'})
318                 return True
319
320         def action_wait(self, cr, uid, ids, *args):
321                 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
322                 event_obj = self.pool.get('res.partner.event')
323                 for o in self.browse(cr, uid, ids):
324                         if event_p:
325                                 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
326                                                 'partner_id': o.partner_id.id,\
327                                                 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
328                                                 'user_id': (o.user_id and o.user_id.id) or uid,\
329                                                 'partner_type': 'customer', 'probability': 1.0,\
330                                                 'planned_revenue': o.amount_untaxed})
331                         if (o.order_policy == 'manual') and (not o.invoice_ids):
332                                 self.write(cr, uid, [o.id], {'state': 'manual'})
333                         else:
334                                 self.write(cr, uid, [o.id], {'state': 'progress'})
335                         self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
336
337         def procurement_lines_get(self, cr, uid, ids, *args):
338                 res = []
339                 for order in self.browse(cr, uid, ids, context={}):
340                         for line in order.order_line:
341                                 if line.procurement_id:
342                                         res.append(line.procurement_id.id)
343                 return res
344
345         # if mode == 'finished':
346         #   returns True if all lines are done, False otherwise
347         # if mode == 'canceled':
348         #       returns True if there is at least one canceled line, False otherwise
349         def test_state(self, cr, uid, ids, mode, *args):
350                 assert mode in ('finished', 'canceled'), "invalid mode for test_state"
351                 finished = True
352                 canceled = False
353                 write_done_ids = []
354                 write_cancel_ids = []
355                 for order in self.browse(cr, uid, ids, context={}):
356                         for line in order.order_line:
357                                 if line.procurement_id and (line.procurement_id.state != 'done') and (line.state!='done'):
358                                         finished = False
359                                 if line.procurement_id and line.procurement_id.state == 'cancel':
360                                         canceled = True
361                                 # if a line is finished (ie its procuremnt is done or it has not procuremernt and it
362                                 # is not already marked as done, mark it as being so...
363                                 if ((not line.procurement_id) or line.procurement_id.state == 'done') and line.state != 'done':
364                                         write_done_ids.append(line.id)
365                                 # ... same for canceled lines
366                                 if line.procurement_id and line.procurement_id.state == 'cancel' and line.state != 'cancel':
367                                         write_cancel_ids.append(line.id)
368                 if write_done_ids:
369                         self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
370                 if write_cancel_ids:
371                         self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'cancel'})
372
373                 if mode=='finished':
374                         return finished
375                 elif mode=='canceled':
376                         return canceled
377
378         def action_ship_create(self, cr, uid, ids, *args):
379                 picking_id=False
380                 for order in self.browse(cr, uid, ids, context={}):
381                         output_id = order.shop_id.warehouse_id.lot_output_id.id
382                         picking_id = False
383                         for line in order.order_line:
384                                 proc_id=False
385                                 date_planned = (DateTime.now() + DateTime.RelativeDateTime(days=line.delay or 0.0)).strftime('%Y-%m-%d')
386                                 if line.state == 'done':
387                                         continue
388                                 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
389                                         location_id = order.shop_id.warehouse_id.lot_stock_id.id
390                                         if not picking_id:
391                                                 loc_dest_id = order.partner_id.property_stock_customer.id
392                                                 picking_id = self.pool.get('stock.picking').create(cr, uid, {
393                                                         'origin': order.name,
394                                                         'type': 'out',
395                                                         'state': 'auto',
396                                                         'move_type': order.picking_policy,
397                                                         'loc_move_id': loc_dest_id,
398                                                         'sale_id': order.id,
399                                                         'address_id': order.partner_shipping_id.id,
400                                                         'note': order.note,
401                                                         'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
402
403                                                 })
404
405                                         move_id = self.pool.get('stock.move').create(cr, uid, {
406                                                 'name':line.name,
407                                                 'picking_id': picking_id,
408                                                 'product_id': line.product_id.id,
409                                                 'date_planned': date_planned,
410                                                 'product_qty': line.product_uom_qty,
411                                                 'product_uom': line.product_uom.id,
412                                                 'product_uos_qty': line.product_uos_qty,
413                                                 'product_uos': (line.product_uos and line.product_uos.id)\
414                                                                 or line.product_uom.id,
415                                                 'product_packaging' : line.product_packaging.id,
416                                                 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id,
417                                                 'location_id': location_id,
418                                                 'location_dest_id': output_id,
419                                                 'sale_line_id': line.id,
420                                                 'tracking_id': False,
421                                                 'state': 'waiting',
422                                                 'note': line.notes,
423                                         })
424                                         proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
425                                                 'name': order.name,
426                                                 'origin': order.name,
427                                                 'date_planned': date_planned,
428                                                 'product_id': line.product_id.id,
429                                                 'product_qty': line.product_uom_qty,
430                                                 'product_uom': line.product_uom.id,
431                                                 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
432                                                 'procure_method': line.type,
433                                                 'move_id': move_id, 
434                                         })
435                                         wf_service = netsvc.LocalService("workflow")
436                                         wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
437                                         self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
438                                 elif line.product_id and line.product_id.product_tmpl_id.type=='service':
439                                         proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
440                                                 'name': line.name,
441                                                 'origin': order.name,
442                                                 'date_planned': date_planned,
443                                                 'product_id': line.product_id.id,
444                                                 'product_qty': line.product_uom_qty,
445                                                 'product_uom': line.product_uom.id,
446                                                 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
447                                                 'procure_method': line.type,
448                                         })
449                                         wf_service = netsvc.LocalService("workflow")
450                                         wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
451                                         self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
452                                 else:
453                                         #
454                                         # No procurement because no product in the sale.order.line.
455                                         #
456                                         pass
457
458                         val = {}
459                         if picking_id:
460                                 wf_service = netsvc.LocalService("workflow")
461                                 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
462                                 #val = {'picking_ids':[(6,0,[picking_id])]}
463
464                         if order.state=='shipping_except':
465                                 val['state'] = 'progress'
466                                 if (order.order_policy == 'manual') and order.invoice_ids:
467                                         val['state'] = 'manual'
468                         self.write(cr, uid, [order.id], val)
469                 return True
470
471         def action_ship_end(self, cr, uid, ids, context={}):
472                 for order in self.browse(cr, uid, ids):
473                         val = {'shipped':True}
474                         if order.state=='shipping_except':
475                                 if (order.order_policy=='manual') and not order.invoice_ids:
476                                         val['state'] = 'manual'
477                                 else:
478                                         val['state'] = 'progress'
479                         self.write(cr, uid, [order.id], val)
480                 return True
481
482         def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
483                 invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed'])
484                 for inv in invs:
485                         part=inv['partner_id'] and inv['partner_id'][0]
486                         pr = inv['amount_untaxed'] or 0.0
487                         partnertype = 'customer'
488                         eventtype = 'sale'
489                         self.pool.get('res.partner.event').create(cr, uid, {'name':'Order: '+name, 'som':False, 'description':'Order '+str(inv['id']), 'document':'', 'partner_id':part, 'date':time.strftime('%Y-%m-%d'), 'canal_id':False, 'user_id':uid, 'partner_type':partnertype, 'probability':1.0, 'planned_revenue':pr, 'planned_cost':0.0, 'type':eventtype})
490
491         def has_stockable_products(self,cr, uid, ids, *args):
492                 for order in self.browse(cr, uid, ids):
493                         for order_line in order.order_line:
494                                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
495                                         return True
496                 return False
497 sale_order()
498
499 # TODO add a field price_unit_uos
500 # - update it on change product and unit price
501 # - use it in report if there is a uos
502 class sale_order_line(osv.osv):
503         def copy(self, cr, uid, id, default=None, context={}):
504                 if not default: default = {}
505                 default.update( {'invoice_lines':[]})
506                 return super(sale_order_line, self).copy(cr, uid, id, default, context)
507
508         def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
509                 res = {}
510                 for line in self.browse(cr, uid, ids):
511                         res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
512                 return res
513
514         def _amount_line(self, cr, uid, ids, field_name, arg, context):
515                 res = {}
516                 cur_obj=self.pool.get('res.currency')
517                 for line in self.browse(cr, uid, ids):
518                         res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
519                         cur = line.order_id.pricelist_id.currency_id
520                         res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
521                 return res
522
523         def _number_packages(self, cr, uid, ids, field_name, arg, context):
524                 res = {}
525                 for line in self.browse(cr, uid, ids):
526                         res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
527                 return res
528         
529         def _get_1st_packaging(self, cr, uid, context={}):
530                 cr.execute('select id from product_packaging order by id asc limit 1')
531                 res = cr.fetchone()
532                 if not res:
533                         return False
534                 return res[0]
535
536         _name = 'sale.order.line'
537         _description = 'Sale Order line'
538         _columns = {
539                 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
540                 'name': fields.char('Description', size=256, required=True, select=True),
541                 'sequence': fields.integer('Sequence'),
542                 'delay': fields.float('Delivery Delay', required=True),
543                 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True),
544                 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines', readonly=True),
545                 'invoiced': fields.boolean('Invoiced', readonly=True),
546                 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
547                 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
548                 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
549                 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
550                 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
551                 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True),
552                 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
553                 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'),
554                 'product_uom_qty': fields.float('Quantity (UOM)', digits=(16,2), required=True),
555                 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
556                 'product_uos_qty': fields.float('Quantity (UOS)'),
557                 'product_uos': fields.many2one('product.uom', 'Product UOS'),
558                 'product_packaging': fields.many2one('product.packaging', 'Packaging used'),
559                 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
560                 'discount': fields.float('Discount (%)', digits=(16,2)),
561                 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number packages'),
562                 'notes': fields.text('Notes'),
563                 'th_weight' : fields.float('Weight'),
564                 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'State', required=True, readonly=True),
565         }
566         _order = 'sequence'
567         _defaults = {
568                 'discount': lambda *a: 0.0,
569                 'delay': lambda *a: 0.0,
570                 'product_uom_qty': lambda *a: 1,
571                 'product_uos_qty': lambda *a: 1,
572                 'sequence': lambda *a: 10,
573                 'invoiced': lambda *a: 0,
574                 'state': lambda *a: 'draft',
575                 'type': lambda *a: 'make_to_stock',
576                 'product_packaging': _get_1st_packaging,
577         }
578         def invoice_line_create(self, cr, uid, ids, context={}):
579                 def _get_line_qty(line):
580                         if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
581                                 return line.product_uos_qty or line.product_uom_qty
582                         else:
583                                 return self.pool.get('mrp.procurement').quantity_get(cr, uid, line.procurement_id.id, context)
584                 create_ids = []
585                 for line in self.browse(cr, uid, ids, context):
586                         if not line.invoiced:
587                                 if line.product_id:
588                                         a =  line.product_id.product_tmpl_id.property_account_income.id
589                                         if not a:
590                                                 a = line.product_id.categ_id.property_account_income_categ.id
591                                         if not a:
592                                                 raise osv.except_osv('Error !', 'There is no income account defined for this product: "%s" (id:%d)' % (line.product_id.name, line.product_id.id,))
593                                 else:
594                                         a = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
595                                 uosqty = _get_line_qty(line)
596                                 uos_id = (line.product_uos and line.product_uos.id) or line.product_uom.id
597                                 pu = line.price_unit
598                                 if line.product_uos_qty:
599                                         pu = round(pu * line.product_uom_qty / line.product_uos_qty, int(config['price_accuracy']))
600                                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
601                                         'name': line.name,
602                                         'account_id': a,
603                                         'price_unit': pu,
604                                         'quantity': uosqty,
605                                         'discount': line.discount,
606                                         'uos_id': uos_id,
607                                         'product_id': line.product_id.id or False,
608                                         'invoice_line_tax_id': [(6,0,[x.id for x in line.tax_id])],
609                                         'note': line.notes,
610                                         'account_analytic_id': line.order_id.project_id.id,
611                                 })
612                                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%d,%d)', (line.id, inv_id))
613                                 self.write(cr, uid, [line.id], {'invoiced':True})
614                                 create_ids.append(inv_id)
615                 return create_ids
616
617         def button_confirm(self, cr, uid, ids, context={}):
618                 return self.write(cr, uid, ids, {'state':'confirmed'})
619
620         def button_done(self, cr, uid, ids, context={}):
621                 wf_service = netsvc.LocalService("workflow")
622                 res = self.write(cr, uid, ids, {'state':'done'})
623                 for line in self.browse(cr,uid,ids,context):
624                         wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
625
626                 return res
627
628         def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
629                 product_obj = self.pool.get('product.product')
630                 if not product_id:
631                         return {'value': {'product_uom': product_uos,
632                                 'product_uom_qty': product_uos_qty}, 'domain':{}}
633
634                 product = product_obj.browse(cr, uid, product_id)
635                 value = {
636                         'product_uom' : product.uom_id,
637                 }
638                 # FIXME must depend on uos/uom of the product and not only of the coeff.
639                 try:
640                         value.update({
641                                 'product_uom_qty' : product_uos_qty / product.uos_coeff,
642                                 'th_weight' : product_uos_qty / product.uos_coeff * product.weight
643                         })
644                 except ZeroDivisionError:
645                         pass
646                 return {'value' : value}
647
648         def copy(self, cr, uid, id, default=None,context={}):
649                 if not default:
650                         default = {}
651                 default.update({'state':'draft', 'move_ids':[], 'invoiced':False, 'invoice_lines':[]})
652                 return super(sale_order_line, self).copy(cr, uid, id, default, context)
653
654         def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
655                         uom=False, qty_uos=0, uos=False, name='', partner_id=False,
656                         lang=False, update_tax=True):
657                 product_uom_obj = self.pool.get('product.uom')
658                 partner_obj = self.pool.get('res.partner')
659                 product_obj = self.pool.get('product.product')
660
661                 if partner_id:
662                         lang = partner_obj.browse(cr, uid, partner_id).lang
663                 context = {'lang': lang}
664
665                 if not product:
666                         return {'value': {'price_unit': 0.0, 'notes':'', 'th_weight' : 0,
667                                 'product_uos_qty': qty}, 'domain': {'product_uom': []}}
668
669                 if not pricelist:
670                         raise osv.except_osv('No Pricelist !',
671                                         'You have to select a pricelist in the sale form !\n'
672                                         'Please set one before choosing a product.')
673
674                 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
675                                 product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
676                 if price is False:
677                         raise osv.except_osv('No valid pricelist line found !',
678                                         "Couldn't find a pricelist line matching this product and quantity.\n"
679                                         "You have to change either the product, the quantity or the pricelist.")
680
681                 product = product_obj.browse(cr, uid, product, context=context)
682
683                 if uom:
684                         uom2 = product_uom_obj.browse(cr, uid, uom)
685                         if product.uom_id.category_id.id <> uom2.category_id.id:
686                                 uom = False
687
688                 if uos:
689                         if product.uos_id:
690                                 uos2 = product_uom_obj.browse(cr, uid, uos)
691                                 if product.uos_id.category_id.id <> uos2.category_id.id:
692                                         uos = False
693                         else:
694                                 uos = False
695
696                 result = {'price_unit': price, 'type': product.procure_method,
697                                 'notes': product.description_sale}
698
699                 if update_tax: #The quantity only have changed
700                         result['delay'] = (product.sale_delay or 0.0)
701                         taxes = self.pool.get('account.tax').browse(cr, uid,
702                                         [x.id for x in product.taxes_id])
703                         taxep = None
704                         if partner_id:
705                                 taxep = self.pool.get('res.partner').browse(cr, uid,
706                                                 partner_id).property_account_tax
707                         if not taxep or not taxep.id:
708                                 result['tax_id'] = [x.id for x in product.taxes_id]
709                         else:
710                                 res5 = [taxep.id]
711                                 for t in taxes:
712                                         if not t.tax_group==taxep.tax_group:
713                                                 res5.append(t.id)
714                                 result['tax_id'] = res5
715
716                 result['name'] = product.partner_ref
717
718                 domain = {}
719                 if not uom and not uos:
720                         result['product_uom'] = product.uom_id.id
721                         if product.uos_id:
722                                 result['product_uos'] = product.uos_id.id
723                                 result['product_uos_qty'] = qty * product.uos_coeff
724                         else:
725                                 result['product_uos'] = False
726                                 result['product_uos_qty'] = qty
727                         result['th_weight'] = qty * product.weight
728                         domain = {'product_uom':
729                                                 [('category_id', '=', product.uom_id.category_id.id)]}
730                 elif uom: # whether uos is set or not
731                         default_uom = product.uom_id and product.uom_id.id
732                         q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
733                         if product.uos_id:
734                                 result['product_uos'] = product.uos_id.id
735                                 result['product_uos_qty'] = q * product.uos_coeff
736                         else:
737                                 result['product_uos'] = False
738                                 result['product_uos_qty'] = q
739                         result['th_weight'] = q * product.weight
740                 elif uos: # only happens if uom is False
741                         result['product_uom'] = product.uom_id and product.uom_id.id
742                         result['product_uom_qty'] = qty_uos / product.uos_coeff
743                         result['th_weight'] = result['product_uom_qty'] * product.weight
744                 return {'value': result, 'domain': domain}
745
746 sale_order_line()