Removed unused one2many from sale order to sale order payment(Which doesn't exist...
[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                 id_set = ",".join(map(str, ids))
72                 cr.execute("SELECT s.id,COALESCE(SUM(l.price_unit*l.product_uos_qty*(100-l.discount))/100.0,0) AS amount FROM sale_order s LEFT OUTER JOIN sale_order_line l ON (s.id=l.order_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
73                 res = dict(cr.fetchall())
74                 cur_obj=self.pool.get('res.currency')
75                 for id in res.keys():
76                         order=self.browse(cr, uid, [id])[0]
77                         cur=order.pricelist_id.currency_id
78                         res[id]=cur_obj.round(cr, uid, cur, res[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_uos_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 are automatically setted when a cancel operation occurs in the invoice validation (Invoice Exception) or in picking list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting the 'Date Order' the schedule.", 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)]}, relate=True, select=True),
123                 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, relate=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')], 'Picking 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 pickings'),
135                 ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The Shipping Policy is used to synchronise invoive and delivery operations. The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice. The 'Shipping & Manual Invoice' will create the picking order directly and wait the user to manually click on the 'Invoice Button' to generate the draft invoice. The 'Invoice after delivery' choice will generate the draft invoice after the picking list have been finished"),
136                 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}),
137                 'project_id':fields.many2one('account.analytic.account', 'Profit/Cost Center', readonly=True, states={'draft':[('readonly', False)]}),
138
139                 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}),
140                 '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)."),
141                 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
142
143                 'shipped':fields.boolean('Picked', readonly=True),
144                 'invoiced':fields.boolean('Paid', readonly=True),
145
146                 'note': fields.text('Notes'),
147
148                 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
149                 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
150                 'amount_total': fields.function(_amount_total, method=True, string='Total'),
151                 '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."),
152         }
153         _defaults = {
154                 'picking_policy': lambda *a: 'direct',
155                 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
156                 'order_policy': lambda *a: 'manual',
157                 'state': lambda *a: 'draft',
158                 'user_id': lambda obj, cr, uid, context: uid,
159                 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
160                 'invoice_quantity': lambda *a: 'order'
161         }
162         _order = 'name desc'
163
164         # Form filling
165         def onchange_shop_id(self, cr, uid, ids, shop_id):
166                 v={}
167                 if shop_id:
168                         shop=self.pool.get('sale.shop').browse(cr,uid,shop_id)
169                         v['project_id']=shop.project_id.id
170                         # Que faire si le client a une pricelist a lui ?
171                         if shop.pricelist_id.id:
172                                 v['pricelist_id']=shop.pricelist_id.id
173                         #v['payment_default_id']=shop.payment_default_id.id
174                 return {'value':v}
175
176         def action_cancel_draft(self, cr, uid, ids, *args):
177                 if not len(ids):
178                         return False
179                 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
180                 line_ids = map(lambda x: x[0], cr.fetchall())
181                 self.write(cr, uid, ids, {'state':'draft', 'invoice_ids':[], 'shipped':0, 'invoiced':0})
182                 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced':False, 'state':'draft', 'invoice_lines':[(6,0,[])]})
183                 wf_service = netsvc.LocalService("workflow")
184                 for inv_id in ids:
185                         wf_service.trg_create(uid, 'sale.order', inv_id, cr)
186                 return True
187
188         def onchange_partner_id(self, cr, uid, ids, part):
189                 if not part:
190                         return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False}}
191                 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact'])
192                 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist[0]
193                 return {'value':{'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'pricelist_id': pricelist}}
194
195         def button_dummy(self, cr, uid, ids, context={}):
196                 return True
197
198 #FIXME: the method should return the list of invoices created (invoice_ids) 
199 # and not the id of the last invoice created (res). The problem is that we 
200 # cannot change it directly since the method is called by the sale order
201 # workflow and I suppose it expects a single id...
202         def _inv_get(self, cr, uid, order, context={}):
203                 return {}
204
205         def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done']):
206                 res = False
207                 invoices = {}
208                 invoice_ids = []
209
210                 def make_invoice(order, lines):
211                         a = order.partner_id.property_account_receivable[0]
212                         if order.partner_id and order.partner_id.property_payment_term:
213                                 pay_term = order.partner_id.property_payment_term[0]
214                         else:
215                                 pay_term = False
216                         for preinv in order.invoice_ids:
217                                 if preinv.state in ('open','paid','proforma'):
218                                         for preline in preinv.invoice_line:
219                                                 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id':False, 'price_unit':-preline.price_unit})
220                                                 lines.append(inv_line_id)
221                         inv = {
222                                 'name': order.name,
223                                 'origin': order.name,
224                                 'type': 'out_invoice',
225                                 'reference': "P%dSO%d"%(order.partner_id.id,order.id),
226                                 'account_id': a,
227                                 'partner_id': order.partner_id.id,
228                                 'address_invoice_id': order.partner_invoice_id.id,
229                                 'address_contact_id': order.partner_invoice_id.id,
230                                 'invoice_line': [(6,0,lines)],
231                                 'currency_id' : order.pricelist_id.currency_id.id,
232                                 'comment': order.note,
233                                 'payment_term': pay_term,
234                         }
235                         inv.update(self._inv_get(cr, uid, order))
236                         inv_obj = self.pool.get('account.invoice')
237                         inv_id = inv_obj.create(cr, uid, inv)
238                         inv_obj.button_compute(cr, uid, [inv_id])
239                         return inv_id
240
241                 for o in self.browse(cr,uid,ids):
242                         lines = []
243                         for line in o.order_line:
244                                 if (line.state in states) and not line.invoiced:
245                                         lines.append(line.id)
246                         created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
247                         if created_lines:
248                                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
249
250                 if not invoices:
251                         for o in self.browse(cr, uid, ids):
252                                 for i in o.invoice_ids:
253                                         if i.state == 'draft':
254                                                 return i.id
255
256                 for val in invoices.values():
257                         if grouped:
258                                 res = make_invoice(val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []))
259                                 for o,l in val:
260                                         self.write(cr, uid, [o.id], {'state' : 'progress'})
261                                         cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
262                         else:
263                                 for order, il in val:
264                                         res = make_invoice(order, il)
265                                         invoice_ids.append(res)
266                                         self.write(cr, uid, [order.id], {'state' : 'progress'})
267                                         cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
268                 return res
269
270         def action_invoice_cancel(self, cr, uid, ids, context={}):
271                 for sale in self.browse(cr, uid, ids):
272                         for line in sale.order_line:
273                                 invoiced=False
274                                 for iline in line.invoice_lines:
275                                         if iline.invoice_id and iline.invoice_id.state == 'cancel':
276                                                 continue
277                                         else:
278                                                 invoiced=True
279                                 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
280                 self.write(cr, uid, ids, {'state':'invoice_except', 'invoice_id':False})
281                 return True
282
283
284         def action_cancel(self, cr, uid, ids, context={}):
285                 ok = True
286                 for sale in self.browse(cr, uid, ids):
287                         for pick in sale.picking_ids:
288                                 if pick.state not in ('draft','cancel'):
289                                         raise osv.except_osv(
290                                                 'Could not cancel sale order !',
291                                                 'You must first cancel all pickings attached to this sale order.')
292                         for r in self.read(cr,uid,ids,['picking_ids']):
293                                 for pick in r['picking_ids']:
294                                         wf_service = netsvc.LocalService("workflow")
295                                         wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
296                         for inv in sale.invoice_ids:
297                                 if inv.state not in ('draft','cancel'):
298                                         raise osv.except_osv(
299                                                 'Could not cancel this sale order !',
300                                                 'You must first cancel all invoices attached to this sale order.')
301                         for r in self.read(cr,uid,ids,['invoice_ids']):
302                                 for inv in r['invoice_ids']:
303                                         wf_service = netsvc.LocalService("workflow")
304                                         wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
305                 self.write(cr,uid,ids,{'state':'cancel'})
306                 return True
307
308         def action_wait(self, cr, uid, ids, *args):
309                 for r in self.read(cr,uid,ids,['order_policy','invoice_ids','name','amount_untaxed','partner_id','user_id','order_line']):
310                         if self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open'):
311                                 self.pool.get('res.partner.event').create(cr, uid, {'name':'Sale Order: '+r['name'], 'partner_id':r['partner_id'][0], 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':(r['user_id'] and r['user_id'][0]) or uid, 'partner_type':'customer', 'probability': 1.0, 'planned_revenue':r['amount_untaxed']})
312                         if (r['order_policy']=='manual') and (not r['invoice_ids']):
313                                 self.write(cr,uid,[r['id']],{'state':'manual'})
314                         else:
315                                 self.write(cr,uid,[r['id']],{'state':'progress'})
316                         self.pool.get('sale.order.line').button_confirm(cr, uid, r['order_line'])
317
318         def procurement_lines_get(self, cr, uid, ids, *args):
319                 res = []
320                 for order in self.browse(cr, uid, ids, context={}):
321                         for line in order.order_line:
322                                 if line.procurement_id:
323                                         res.append(line.procurement_id.id)
324                 return res
325
326         # if mode == 'finished':
327         #   returns True if all lines are done, False otherwise
328         # if mode == 'canceled':
329         #       returns True if there is at least one canceled line, False otherwise
330         def test_state(self, cr, uid, ids, mode, *args):
331                 assert mode in ('finished', 'canceled'), "invalid mode for test_state"
332                 finished = True
333                 canceled = False
334                 write_done_ids = []
335                 write_cancel_ids = []
336                 for order in self.browse(cr, uid, ids, context={}):
337                         for line in order.order_line:
338                                 if line.procurement_id and (line.procurement_id.state != 'done') and (line.state!='done'):
339                                         finished = False
340                                 if line.procurement_id and line.procurement_id.state == 'cancel':
341                                         canceled = True
342                                 # if a line is finished (ie its procuremnt is done or it has not procuremernt and it
343                                 # is not already marked as done, mark it as being so...
344                                 if ((not line.procurement_id) or line.procurement_id.state == 'done') and line.state != 'done':
345                                         write_done_ids.append(line.id)
346                                 # ... same for canceled lines
347                                 if line.procurement_id and line.procurement_id.state == 'cancel' and line.state != 'cancel':
348                                         write_cancel_ids.append(line.id)
349                 if write_done_ids:
350                         self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
351                 if write_cancel_ids:
352                         self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'cancel'})
353
354                 if mode=='finished':
355                         return finished
356                 elif mode=='canceled':
357                         return canceled
358
359         def action_ship_create(self, cr, uid, ids, *args):
360                 picking_id=False
361                 for order in self.browse(cr, uid, ids, context={}):
362                         output_id = order.shop_id.warehouse_id.lot_output_id.id
363                         picking_id = False
364                         for line in order.order_line:
365                                 proc_id=False
366                                 date_planned = (DateTime.now() + DateTime.RelativeDateTime(days=line.delay or 0.0)).strftime('%Y-%m-%d')
367                                 if line.state == 'done':
368                                         continue
369                                 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
370                                         location_id = order.shop_id.warehouse_id.lot_stock_id.id
371                                         if not picking_id:
372                                                 loc_dest_id = order.partner_id.property_stock_customer[0]
373                                                 picking_id = self.pool.get('stock.picking').create(cr, uid, {
374                                                         'origin': order.name,
375                                                         'type': 'out',
376                                                         'state': 'auto',
377                                                         'move_type': order.picking_policy,
378                                                         'loc_move_id': loc_dest_id,
379                                                         'sale_id': order.id,
380                                                         'address_id': order.partner_shipping_id.id,
381                                                         'note': order.note,
382                                                         'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
383
384                                                 })
385
386                                         move_id = self.pool.get('stock.move').create(cr, uid, {
387                                                 'name':line.name,
388                                                 'picking_id': picking_id,
389                                                 'product_id': line.product_id.id,
390                                                 'date_planned': date_planned,
391                                                 'product_qty': line.product_uom_qty,
392                                                 'product_uom': line.product_uom.id,
393                                                 'product_uos_qty': line.product_uos_qty,
394                                                 'product_uos': line.product_uos.id,
395                                                 'product_packaging' : line.product_packaging.id,
396                                                 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id,
397                                                 'location_id': location_id,
398                                                 'location_dest_id': output_id,
399                                                 'sale_line_id': line.id,
400                                                 'tracking_id': False,
401                                                 'state': 'waiting',
402                                                 'note': line.notes,
403                                         })
404                                         proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
405                                                 'name': order.name,
406                                                 'origin': order.name,
407                                                 'date_planned': date_planned,
408                                                 'product_id': line.product_id.id,
409                                                 'product_qty': line.product_uom_qty,
410                                                 'product_uom': line.product_uom.id,
411                                                 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
412                                                 'procure_method': line.type,
413                                                 'move_id': move_id, 
414                                         })
415                                         wf_service = netsvc.LocalService("workflow")
416                                         wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
417                                         self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
418                                 elif line.product_id and line.product_id.product_tmpl_id.type=='service':
419                                         proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
420                                                 'name': line.name,
421                                                 'origin': order.name,
422                                                 'date_planned': date_planned,
423                                                 'product_id': line.product_id.id,
424                                                 'product_qty': line.product_uom_qty,
425                                                 'product_uom': line.product_uom.id,
426                                                 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
427                                                 'procure_method': line.type,
428                                         })
429                                         wf_service = netsvc.LocalService("workflow")
430                                         wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
431                                         self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
432                                 else:
433                                         #
434                                         # No procurement because no product in the sale.order.line.
435                                         #
436                                         pass
437
438                         val = {}
439                         if picking_id:
440                                 wf_service = netsvc.LocalService("workflow")
441                                 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
442                                 #val = {'picking_ids':[(6,0,[picking_id])]}
443
444                         if order.state=='shipping_except':
445                                 val['state'] = 'progress'
446                                 if (order.order_policy == 'manual') and order.invoice_ids:
447                                         val['state'] = 'manual'
448                         self.write(cr, uid, [order.id], val)
449                 return True
450
451         def action_ship_end(self, cr, uid, ids, context={}):
452                 for order in self.browse(cr, uid, ids):
453                         val = {'shipped':True}
454                         if order.state=='shipping_except':
455                                 if (order.order_policy=='manual') and not order.invoice_ids:
456                                         val['state'] = 'manual'
457                                 else:
458                                         val['state'] = 'progress'
459                         self.write(cr, uid, [order.id], val)
460                 return True
461
462         def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
463                 invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed'])
464                 for inv in invs:
465                         part=inv['partner_id'] and inv['partner_id'][0]
466                         pr = inv['amount_untaxed'] or 0.0
467                         partnertype = 'customer'
468                         eventtype = 'sale'
469                         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})
470
471         def has_stockable_products(self,cr, uid, ids, *args):
472                 for order in self.browse(cr, uid, ids):
473                         for order_line in order.order_line:
474                                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
475                                         return True
476                 return False
477 sale_order()
478
479 class sale_order_line(osv.osv):
480         def copy(self, cr, uid, id, default=None, context={}):
481                 if not default: default = {}
482                 default.update( {'invoice_lines':[]})
483                 return super(sale_order_line, self).copy(cr, uid, id, default, context)
484
485         def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
486                 res = {}
487                 for line in self.browse(cr, uid, ids):
488                         if line.product_uos.id:
489                                 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) /100.0)
490                         else:
491                                 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
492                 return res
493
494         def _amount_line(self, cr, uid, ids, field_name, arg, context):
495                 res = {}
496                 cur_obj=self.pool.get('res.currency')
497                 for line in self.browse(cr, uid, ids):
498                         if line.product_uos.id:
499                                 res[line.id] = line.price_unit * line.product_uos_qty * (1 - (line.discount or 0.0) /100.0)
500                         else:
501                                 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
502                         cur = line.order_id.pricelist_id.currency_id
503                         res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
504                 return res
505
506         def _number_packages(self, cr, uid, ids, field_name, arg, context):
507                 res = {}
508                 for line in self.browse(cr, uid, ids):
509                         res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
510                 return res
511         
512         def _get_1st_packaging(self, cr, uid, context={}):
513                 cr.execute('select id from product_packaging order by id asc limit 1')
514                 res = cr.fetchone()
515                 if not res:
516                         return False
517                 return res[0]
518
519         _name = 'sale.order.line'
520         _description = 'Sale Order line'
521         _columns = {
522                 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
523                 'name': fields.char('Description', size=256, required=True, select=True),
524                 'sequence': fields.integer('Sequence'),
525                 'delay': fields.float('Delivery Delay', required=True),
526                 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True, relate=True),
527                 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines', readonly=True),
528                 'invoiced': fields.boolean('Invoiced', readonly=True, select=True),
529                 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
530                 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
531                 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
532                 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
533                 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
534                 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True),
535                 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
536                 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'),
537                 'product_uom_qty': fields.float('Quantity (UOM)', digits=(16,2), required=True),
538                 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
539                 'product_uos_qty': fields.float('Quantity (UOS)'),
540                 'product_uos': fields.many2one('product.uom', 'Product UOS'),
541                 'product_packaging': fields.many2one('product.packaging', 'Packaging used'),
542                 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
543                 'discount': fields.float('Discount (%)', digits=(16,2)),
544                 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number packages'),
545                 'notes': fields.text('Notes'),
546                 'th_weight' : fields.float('Weight'),
547                 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'State', required=True, readonly=True),
548                 'price_unit_customer': fields.float('Customer Unit Price', digits=(16, int(config['price_accuracy']))),
549         }
550         _order = 'sequence'
551         _defaults = {
552                 'discount': lambda *a: 0.0,
553                 'delay': lambda *a: 0.0,
554                 'product_uom_qty': lambda *a: 1,
555                 'product_uos_qty': lambda *a: 1,
556                 'sequence': lambda *a: 10,
557                 'invoiced': lambda *a: 0,
558                 'state': lambda *a: 'draft',
559                 'type': lambda *a: 'make_to_stock',
560                 'product_packaging': _get_1st_packaging,
561         }
562         def invoice_line_create(self, cr, uid, ids, context={}):
563                 def _get_line_qty(line):
564                         if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
565                                 return line.product_uos_qty or line.product_uom_qty
566                         else:
567                                 return self.pool.get('mrp.procurement').quantity_get(cr, uid, line.procurement_id.id, context)
568                 create_ids = []
569                 for line in self.browse(cr, uid, ids, context):
570                         if not line.invoiced:
571                                 if line.product_id:
572                                         a =  line.product_id.product_tmpl_id.property_account_income
573                                         if not a:
574                                                 a = line.product_id.categ_id.property_account_income_categ
575                                         if not a:
576                                                 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,))
577                                         a = a[0]
578                                 else:
579                                         a = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
580                                 uosqty = _get_line_qty(line)
581                                 uos_id = (line.product_uos and line.product_uos.id) or False
582                                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
583                                         'name': line.name,
584                                         'account_id': a,
585                                         'price_unit': line.price_unit,
586                                         'quantity': uosqty,
587                                         'discount': line.discount,
588                                         'uos_id': uos_id,
589                                         'product_id': line.product_id.id or False,
590                                         'invoice_line_tax_id': [(6,0,[x.id for x in line.tax_id])],
591                                         'note': line.notes,
592                                         'account_analytic_id': line.order_id.project_id.id,
593                                 })
594                                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%d,%d)', (line.id, inv_id))
595                                 self.write(cr, uid, [line.id], {'invoiced':True})
596                                 create_ids.append(inv_id)
597                 return create_ids
598
599         def button_confirm(self, cr, uid, ids, context={}):
600                 return self.write(cr, uid, ids, {'state':'confirmed'})
601
602         def button_done(self, cr, uid, ids, context={}):
603                 wf_service = netsvc.LocalService("workflow")
604                 res = self.write(cr, uid, ids, {'state':'done'})
605                 for line in self.browse(cr,uid,ids,context):
606                         wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
607
608                 return res
609
610         def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
611                 if not product_id:
612                         return {'value': {'product_uom': product_uos, 'product_uom_qty': product_uos_qty}, 'domain':{}}
613                 res = self.pool.get('product.product').read(cr, uid, [product_id], ['uom_id', 'uos_id', 'uos_coeff', 'weight'])[0]
614                 value = {
615                         'product_uom' : res['uom_id'], 
616                 }
617                 try:
618                         value.update({
619                                 'product_uom_qty' : product_uos_qty / res['uos_coeff'],
620                                 'weight' : product_uos_qty / res['uos_coeff'] * res['weight']
621                         })
622                 except ZeroDivisionError:
623                         pass
624                 return {'value' : value}
625
626         def copy(self, cr, uid, id, default=None,context={}):
627                 if not default:
628                         default = {}
629                 default.update({'state':'draft', 'move_ids':[], 'invoiced':False, 'invoice_lines':[]})
630                 return super(sale_order_line, self).copy(cr, uid, id, default, context)
631
632         def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False):
633                 if partner_id:
634                         lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
635                 context = {'lang':lang}
636                 if not product:
637                         return {'value': {'price_unit': 0.0, 'notes':'', 'weight' : 0}, 'domain':{'product_uom':[]}}
638                 if not pricelist:
639                         raise osv.except_osv('No Pricelist !', 'You have to select a pricelist in the sale form !\nPlease set one before choosing a product.')
640                 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
641                 if price is False:
642                         raise osv.except_osv('No valid pricelist line found !', "Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist.")
643                 res = self.pool.get('product.product').read(cr, uid, [product], context=context)[0]
644 #               dt = (DateTime.now() + DateTime.RelativeDateTime(days=res['sale_delay'] or 0.0)).strftime('%Y-%m-%d')
645
646                 result = {'price_unit': price, 'type':res['procure_method'], 'delay':(res['sale_delay'] or 0.0), 'notes':res['description_sale']}
647
648                 taxes = self.pool.get('account.tax').browse(cr, uid, res['taxes_id'])
649                 taxep = self.pool.get('res.partner').browse(cr, uid, partner_id).property_account_tax
650                 if not taxep:
651                         result['tax_id'] = res['taxes_id']
652                 else:
653                         res5 = [taxep[0]]
654                         tp = self.pool.get('account.tax').browse(cr, uid, taxep[0])
655                         for t in taxes:
656                                 if not t.tax_group==tp.tax_group:
657                                         res5.append(t.id)
658                         result['tax_id'] = res5
659
660                 result['name'] = res['partner_ref']
661                 domain = {}
662                 if not uom and not uos:
663                         result['product_uom'] = res['uom_id'] and res['uom_id'][0]
664                         if result['product_uom']:
665                                 result['product_uos'] = res['uos_id']
666                                 result['product_uos_qty'] = qty * res['uos_coeff']
667                                 result['weight'] = qty * res['weight']
668                                 res2 = self.pool.get('product.uom').read(cr, uid, [result['product_uom']], ['category_id'])
669                                 if res2 and res2[0]['category_id']:
670                                         domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
671                 elif uom: # whether uos is set or not
672                         default_uom = res['uom_id'] and res['uom_id'][0]
673                         q = self.pool.get('product.uom')._compute_qty(cr, uid, uom, qty, default_uom)
674                         result['product_uos'] = res['uos_id']
675                         result['product_uos_qty'] = q * res['uos_coeff']
676                         result['weight'] = q * res['weight']
677                 elif uos: # only happens if uom is False
678                         result['product_uom'] = res['uom_id'] and res['uom_id'][0]
679                         result['product_uom_qty'] = qty_uos / res['uos_coeff']
680                         result['weight'] = result['product_uom_qty'] * res['weight']
681                 return {'value':result, 'domain':domain}
682 sale_order_line()