1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
25 from osv import fields, osv
26 from mx import DateTime
27 from tools import config
28 from tools.translate import _
31 class sale_shop(osv.osv):
33 _description = "Sale Shop"
35 'name': fields.char('Shop Name', size=64, required=True),
36 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
37 'payment_account_id': fields.many2many('account.account', 'sale_shop_account', 'shop_id', 'account_id', 'Payment Accounts'),
38 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
45 def _incoterm_get(self, cr, uid, context={}):
46 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
50 class sale_order(osv.osv):
52 _description = "Sale Order"
54 def copy(self, cr, uid, id, default=None, context={}):
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context)
66 def _amount_line_tax(self, cr, uid, line, context={}):
68 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id):
72 def _amount_all(self, cr, uid, ids, field_name, arg, context):
74 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids):
77 'amount_untaxed': 0.0,
82 cur = order.pricelist_id.currency_id
83 for line in order.order_line:
84 val1 += line.price_subtotal
85 val += self._amount_line_tax(cr, uid, line, context)
86 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
91 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 p.sale_id,sum(m.product_qty), m.state
102 stock_picking p on (p.id=m.picking_id)
104 p.sale_id in ('''+','.join(map(str, ids))+''')
105 GROUP BY m.state, p.sale_id''')
106 for oid, nbr, state in cr.fetchall():
107 if state == 'cancel':
110 res[oid][0] += nbr or 0.0
111 res[oid][1] += nbr or 0.0
113 res[oid][1] += nbr or 0.0
118 res[r] = 100.0 * res[r][0] / res[r][1]
119 for order in self.browse(cr, uid, ids, context):
121 res[order.id] = 100.0
124 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
131 for invoice in sale.invoice_ids:
132 if invoice.state not in ('draft', 'cancel'):
133 tot += invoice.amount_untaxed
136 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
141 def _invoiced(self, cursor, user, ids, name, arg, context=None):
143 for sale in self.browse(cursor, user, ids, context=context):
145 for invoice in sale.invoice_ids:
146 if invoice.state != 'paid':
149 if not sale.invoice_ids:
153 def _invoiced_search(self, cursor, user, obj, name, args):
161 clause += 'AND inv.state = \'paid\''
163 clause += 'AND inv.state <> \'paid\''
165 cursor.execute('SELECT rel.order_id ' \
166 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
167 'WHERE rel.invoice_id = inv.id ' + clause)
168 res = cursor.fetchall()
170 cursor.execute('SELECT sale.id ' \
171 'FROM sale_order AS sale ' \
172 'WHERE sale.id NOT IN ' \
173 '(SELECT rel.order_id ' \
174 'FROM sale_order_invoice_rel AS rel)')
175 res.extend(cursor.fetchall())
177 return [('id', '=', 0)]
178 return [('id', 'in', [x[0] for x in res])]
180 def _get_order(self, cr, uid, ids, context={}):
182 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
183 result[line.order_id.id] = True
187 'name': fields.char('Order Reference', size=64, required=True, select=True),
188 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
189 'origin': fields.char('Origin', size=64),
190 'client_order_ref': fields.char('Customer Ref', size=64),
192 'state': fields.selection([
193 ('draft', 'Quotation'),
194 ('waiting_date', 'Waiting Schedule'),
195 ('manual', 'Manual In Progress'),
196 ('progress', 'In Progress'),
197 ('shipping_except', 'Shipping Exception'),
198 ('invoice_except', 'Invoice Exception'),
200 ('cancel', 'Cancelled')
201 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the packing list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Date Ordered'.", select=True),
202 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
204 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
205 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
206 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
207 'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact that requested the order or quotation."),
208 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
210 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
211 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
212 'Packing Policy', required=True, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
213 'order_policy': fields.selection([
214 ('prepaid', 'Payment Before Delivery'),
215 ('manual', 'Shipping & Manual Invoice'),
216 ('postpaid', 'Invoice on Order After Delivery'),
217 ('picking', 'Invoice from the Packing'),
218 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
219 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
220 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
221 - The 'Shipping & Manual Invoice' will create the packing order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
222 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all packing lists have been finished.
223 - The 'Invoice from the packing' choice is used to create an invoice during the packing process."""),
224 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
225 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
227 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
228 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoice', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
229 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Packing', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
230 'shipped': fields.boolean('Picked', readonly=True),
231 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
232 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
233 'invoiced': fields.function(_invoiced, method=True, string='Paid',
234 fnct_search=_invoiced_search, type='boolean'),
235 'note': fields.text('Notes'),
237 'amount_untaxed': fields.function(_amount_all, method=True, string='Untaxed Amount',
239 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
240 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
243 'amount_tax': fields.function(_amount_all, method=True, string='Taxes',
245 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
246 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
249 'amount_total': fields.function(_amount_all, method=True, string='Total',
251 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
252 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
256 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True),
257 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
258 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
261 'picking_policy': lambda *a: 'direct',
262 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
263 'order_policy': lambda *a: 'manual',
264 'state': lambda *a: 'draft',
265 'user_id': lambda obj, cr, uid, context: uid,
266 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
267 'invoice_quantity': lambda *a: 'order',
268 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
269 'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
270 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
271 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist.id,
276 def unlink(self, cr, uid, ids, context=None):
277 sale_orders = self.read(cr, uid, ids, ['state'])
279 for s in sale_orders:
280 if s['state'] in ['draft', 'cancel']:
281 unlink_ids.append(s['id'])
283 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
284 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
286 def onchange_shop_id(self, cr, uid, ids, shop_id):
289 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
290 v['project_id'] = shop.project_id.id
291 # Que faire si le client a une pricelist a lui ?
292 if shop.pricelist_id.id:
293 v['pricelist_id'] = shop.pricelist_id.id
294 #v['payment_default_id']=shop.payment_default_id.id
297 def action_cancel_draft(self, cr, uid, ids, *args):
300 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
301 line_ids = map(lambda x: x[0], cr.fetchall())
302 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
303 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
304 wf_service = netsvc.LocalService("workflow")
306 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
309 def onchange_partner_id(self, cr, uid, ids, part):
311 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
312 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
313 part = self.pool.get('res.partner').browse(cr, uid, part)
314 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
315 payment_term = part.property_payment_term and part.property_payment_term.id or False
316 fiscal_position = part.property_account_position and part.property_account_position.id or False
317 val = {'partner_invoice_id': addr['invoice'], 'partner_order_id': addr['contact'], 'partner_shipping_id': addr['delivery'], 'payment_term': payment_term, 'fiscal_position': fiscal_position}
319 val['pricelist_id'] = pricelist
320 return {'value': val}
322 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
326 if policy == 'prepaid':
328 elif policy == 'picking':
329 inv_qty = 'procurement'
330 return {'value': {'invoice_quantity': inv_qty}}
332 def write(self, cr, uid, ids, vals, context=None):
333 if 'order_policy' in vals:
334 if vals['order_policy'] == 'prepaid':
335 vals.update({'invoice_quantity': 'order'})
336 elif vals['order_policy'] == 'picking':
337 vals.update({'invoice_quantity': 'procurement'})
338 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
340 def create(self, cr, uid, vals, context={}):
341 if 'order_policy' in vals:
342 if vals['order_policy'] == 'prepaid':
343 vals.update({'invoice_quantity': 'order'})
344 if vals['order_policy'] == 'picking':
345 vals.update({'invoice_quantity': 'procurement'})
346 return super(sale_order, self).create(cr, uid, vals, context=context)
348 def button_dummy(self, cr, uid, ids, context={}):
351 #FIXME: the method should return the list of invoices created (invoice_ids)
352 # and not the id of the last invoice created (res). The problem is that we
353 # cannot change it directly since the method is called by the sale order
354 # workflow and I suppose it expects a single id...
355 def _inv_get(self, cr, uid, order, context={}):
358 def _make_invoice(self, cr, uid, order, lines, context={}):
359 a = order.partner_id.property_account_receivable.id
360 if order.payment_term:
361 pay_term = order.payment_term.id
364 for preinv in order.invoice_ids:
365 if preinv.state not in ('cancel',):
366 for preline in preinv.invoice_line:
367 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
368 lines.append(inv_line_id)
370 'name': order.client_order_ref or order.name,
371 'origin': order.name,
372 'type': 'out_invoice',
373 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
375 'partner_id': order.partner_id.id,
376 'address_invoice_id': order.partner_invoice_id.id,
377 'address_contact_id': order.partner_order_id.id,
378 'invoice_line': [(6, 0, lines)],
379 'currency_id': order.pricelist_id.currency_id.id,
380 'comment': order.note,
381 'payment_term': pay_term,
382 'fiscal_position': order.partner_id.property_account_position.id,
383 'date_invoice' : context.get('date_invoice',False)
385 inv_obj = self.pool.get('account.invoice')
386 inv.update(self._inv_get(cr, uid, order))
387 inv_id = inv_obj.create(cr, uid, inv)
388 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
389 if data.get('value', False):
390 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
391 inv_obj.button_compute(cr, uid, [inv_id])
394 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
400 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
401 # last day of the last month as invoice date
403 context['date_inv'] = date_inv
404 for o in self.browse(cr,uid,ids):
406 for line in o.order_line:
407 if (line.state in states) and not line.invoiced:
408 lines.append(line.id)
409 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
411 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
414 for o in self.browse(cr, uid, ids):
415 for i in o.invoice_ids:
416 if i.state == 'draft':
418 picking_obj = self.pool.get('stock.picking')
419 for val in invoices.values():
421 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
423 self.write(cr, uid, [o.id], {'state' : 'progress'})
424 if o.order_policy=='picking':
425 picking_obj.write(cr,uid,map(lambda x:x.id,o.picking_ids),{'invoice_state':'invoiced'})
426 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
428 for order, il in val:
429 res = self._make_invoice(cr, uid, order, il, context=context)
430 invoice_ids.append(res)
431 self.write(cr, uid, [order.id], {'state': 'progress'})
432 if order.order_policy == 'picking':
433 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
434 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
437 def action_invoice_cancel(self, cr, uid, ids, context={}):
438 for sale in self.browse(cr, uid, ids):
439 for line in sale.order_line:
441 for iline in line.invoice_lines:
442 if iline.invoice_id and iline.invoice_id.state == 'cancel':
446 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
447 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
450 def action_cancel(self, cr, uid, ids, context={}):
452 sale_order_line_obj = self.pool.get('sale.order.line')
453 for sale in self.browse(cr, uid, ids):
454 for pick in sale.picking_ids:
455 if pick.state not in ('draft', 'cancel'):
456 raise osv.except_osv(
457 _('Could not cancel sale order !'),
458 _('You must first cancel all packing attached to this sale order.'))
459 for r in self.read(cr, uid, ids, ['picking_ids']):
460 for pick in r['picking_ids']:
461 wf_service = netsvc.LocalService("workflow")
462 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
463 for inv in sale.invoice_ids:
464 if inv.state not in ('draft', 'cancel'):
465 raise osv.except_osv(
466 _('Could not cancel this sale order !'),
467 _('You must first cancel all invoices attached to this sale order.'))
468 for r in self.read(cr, uid, ids, ['invoice_ids']):
469 for inv in r['invoice_ids']:
470 wf_service = netsvc.LocalService("workflow")
471 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
472 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
474 self.write(cr, uid, ids, {'state': 'cancel'})
477 def action_wait(self, cr, uid, ids, *args):
478 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
479 event_obj = self.pool.get('res.partner.event')
480 for o in self.browse(cr, uid, ids):
482 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
483 'partner_id': o.partner_id.id,\
484 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
485 'user_id': (o.user_id and o.user_id.id) or uid,\
486 'partner_type': 'customer', 'probability': 1.0,\
487 'planned_revenue': o.amount_untaxed})
488 if (o.order_policy == 'manual'):
489 self.write(cr, uid, [o.id], {'state': 'manual'})
491 self.write(cr, uid, [o.id], {'state': 'progress'})
492 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
494 def procurement_lines_get(self, cr, uid, ids, *args):
496 for order in self.browse(cr, uid, ids, context={}):
497 for line in order.order_line:
498 if line.procurement_id:
499 res.append(line.procurement_id.id)
502 # if mode == 'finished':
503 # returns True if all lines are done, False otherwise
504 # if mode == 'canceled':
505 # returns True if there is at least one canceled line, False otherwise
506 def test_state(self, cr, uid, ids, mode, *args):
507 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
512 write_cancel_ids = []
513 stock_move_obj = self.pool.get('stock.move')
515 for order in self.browse(cr, uid, ids, context={}):
517 #check for pending deliveries
518 pending_deliveries = False
520 for line in order.order_line:
521 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
522 for move in stock_move_obj.browse( cr, uid, move_ids ):
523 #if one of the related order lines is in state draft, auto or confirmed
524 #this order line is not yet delivered
525 if move.state in ('draft', 'auto', 'confirmed'):
526 pending_deliveries = True
528 if ((not line.procurement_id) or (line.procurement_id.state=='done')) and not pending_deliveries:
530 if line.state != 'done':
531 write_done_ids.append(line.id)
534 if line.procurement_id:
535 if (line.procurement_id.state == 'cancel'):
537 if line.state != 'exception':
538 write_cancel_ids.append(line.id)
543 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
545 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
547 if mode == 'finished':
549 elif mode == 'canceled':
555 def action_ship_create(self, cr, uid, ids, *args):
557 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
558 for order in self.browse(cr, uid, ids, context={}):
559 output_id = order.shop_id.warehouse_id.lot_output_id.id
561 for line in order.order_line:
563 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
564 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
565 if line.state == 'done':
567 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
568 location_id = order.shop_id.warehouse_id.lot_stock_id.id
570 loc_dest_id = order.partner_id.property_stock_customer.id
571 picking_id = self.pool.get('stock.picking').create(cr, uid, {
572 'origin': order.name,
575 'move_type': order.picking_policy,
577 'address_id': order.partner_shipping_id.id,
579 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
583 move_id = self.pool.get('stock.move').create(cr, uid, {
584 'name': line.name[:64],
585 'picking_id': picking_id,
586 'product_id': line.product_id.id,
587 'date_planned': date_planned,
588 'product_qty': line.product_uom_qty,
589 'product_uom': line.product_uom.id,
590 'product_uos_qty': line.product_uos_qty,
591 'product_uos': (line.product_uos and line.product_uos.id)\
592 or line.product_uom.id,
593 'product_packaging': line.product_packaging.id,
594 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
595 'location_id': location_id,
596 'location_dest_id': output_id,
597 'sale_line_id': line.id,
598 'tracking_id': False,
603 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
605 'origin': order.name,
606 'date_planned': date_planned,
607 'product_id': line.product_id.id,
608 'product_qty': line.product_uom_qty,
609 'product_uom': line.product_uom.id,
610 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
611 or line.product_uom_qty,
612 'product_uos': (line.product_uos and line.product_uos.id)\
613 or line.product_uom.id,
614 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
615 'procure_method': line.type,
617 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
619 wf_service = netsvc.LocalService("workflow")
620 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
621 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
622 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
623 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
625 'origin': order.name,
626 'date_planned': date_planned,
627 'product_id': line.product_id.id,
628 'product_qty': line.product_uom_qty,
629 'product_uom': line.product_uom.id,
630 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
631 'procure_method': line.type,
632 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
634 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
635 wf_service = netsvc.LocalService("workflow")
636 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
639 # No procurement because no product in the sale.order.line.
645 wf_service = netsvc.LocalService("workflow")
646 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
648 if order.state == 'shipping_except':
649 val['state'] = 'progress'
651 if (order.order_policy == 'manual'):
652 for line in order.order_line:
653 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
654 val['state'] = 'manual'
656 self.write(cr, uid, [order.id], val)
660 def action_ship_end(self, cr, uid, ids, context={}):
661 for order in self.browse(cr, uid, ids):
662 val = {'shipped': True}
663 if order.state == 'shipping_except':
664 val['state'] = 'progress'
665 if (order.order_policy == 'manual'):
666 for line in order.order_line:
667 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
668 val['state'] = 'manual'
670 for line in order.order_line:
672 if line.state == 'exception':
673 towrite.append(line.id)
675 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
676 self.write(cr, uid, [order.id], val)
679 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
680 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
682 part = inv['partner_id'] and inv['partner_id'][0]
683 pr = inv['amount_untaxed'] or 0.0
684 partnertype = 'customer'
687 'name': 'Order: '+name,
689 'description': 'Order '+str(inv['id']),
692 'date': time.strftime('%Y-%m-%d'),
695 'partner_type': partnertype,
697 'planned_revenue': pr,
701 self.pool.get('res.partner.event').create(cr, uid, event)
703 def has_stockable_products(self, cr, uid, ids, *args):
704 for order in self.browse(cr, uid, ids):
705 for order_line in order.order_line:
706 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
711 # TODO add a field price_unit_uos
712 # - update it on change product and unit price
713 # - use it in report if there is a uos
714 class sale_order_line(osv.osv):
715 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
717 for line in self.browse(cr, uid, ids):
718 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
721 def _amount_line(self, cr, uid, ids, field_name, arg, context):
723 cur_obj = self.pool.get('res.currency')
724 for line in self.browse(cr, uid, ids):
725 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
726 cur = line.order_id.pricelist_id.currency_id
727 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
730 def _number_packages(self, cr, uid, ids, field_name, arg, context):
732 for line in self.browse(cr, uid, ids):
734 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
739 _name = 'sale.order.line'
740 _description = 'Sale Order line'
742 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
743 'name': fields.char('Description', size=256, required=True, select=True),
744 'sequence': fields.integer('Sequence'),
745 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the the shipping of the products to the customer"),
746 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
747 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
748 'invoiced': fields.boolean('Invoiced', readonly=True),
749 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
750 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
751 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
752 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
753 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
754 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True),
755 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
756 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
757 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
758 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
759 'product_uos_qty': fields.float('Quantity (UoS)'),
760 'product_uos': fields.many2one('product.uom', 'Product UoS'),
761 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
762 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
763 'discount': fields.float('Discount (%)', digits=(16, 2)),
764 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
765 'notes': fields.text('Notes'),
766 'th_weight': fields.float('Weight'),
767 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
768 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
770 _order = 'sequence, id'
772 'discount': lambda *a: 0.0,
773 'delay': lambda *a: 0.0,
774 'product_uom_qty': lambda *a: 1,
775 'product_uos_qty': lambda *a: 1,
776 'sequence': lambda *a: 10,
777 'invoiced': lambda *a: 0,
778 'state': lambda *a: 'draft',
779 'type': lambda *a: 'make_to_stock',
780 'product_packaging': lambda *a: False
783 def invoice_line_create(self, cr, uid, ids, context={}):
784 def _get_line_qty(line):
785 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
787 return line.product_uos_qty or 0.0
788 return line.product_uom_qty
790 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
791 line.procurement_id.id, context)
793 def _get_line_uom(line):
794 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
796 return line.product_uos.id
797 return line.product_uom.id
799 return self.pool.get('mrp.procurement').uom_get(cr, uid,
800 line.procurement_id.id, context)
804 for line in self.browse(cr, uid, ids, context):
805 if not line.invoiced:
807 a = line.product_id.product_tmpl_id.property_account_income.id
809 a = line.product_id.categ_id.property_account_income_categ.id
811 raise osv.except_osv(_('Error !'),
812 _('There is no income account defined ' \
813 'for this product: "%s" (id:%d)') % \
814 (line.product_id.name, line.product_id.id,))
816 a = self.pool.get('ir.property').get(cr, uid,
817 'property_account_income_categ', 'product.category',
819 uosqty = _get_line_qty(line)
820 uos_id = _get_line_uom(line)
823 pu = round(line.price_unit * line.product_uom_qty / uosqty,
824 int(config['price_accuracy']))
825 fpos = line.order_id.fiscal_position or False
826 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
827 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
829 'origin': line.order_id.name,
833 'discount': line.discount,
835 'product_id': line.product_id.id or False,
836 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
838 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
840 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
841 self.write(cr, uid, [line.id], {'invoiced': True})
843 sales[line.order_id.id] = True
844 create_ids.append(inv_id)
846 # Trigger workflow events
847 wf_service = netsvc.LocalService("workflow")
848 for sid in sales.keys():
849 wf_service.trg_write(uid, 'sale.order', sid, cr)
852 def button_cancel(self, cr, uid, ids, context={}):
853 for line in self.browse(cr, uid, ids, context=context):
855 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
856 return self.write(cr, uid, ids, {'state': 'cancel'})
858 def button_confirm(self, cr, uid, ids, context={}):
859 return self.write(cr, uid, ids, {'state': 'confirmed'})
861 def button_done(self, cr, uid, ids, context={}):
862 wf_service = netsvc.LocalService("workflow")
863 res = self.write(cr, uid, ids, {'state': 'done'})
864 for line in self.browse(cr, uid, ids, context):
865 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
869 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
870 product_obj = self.pool.get('product.product')
872 return {'value': {'product_uom': product_uos,
873 'product_uom_qty': product_uos_qty}, 'domain': {}}
875 product = product_obj.browse(cr, uid, product_id)
877 'product_uom': product.uom_id.id,
879 # FIXME must depend on uos/uom of the product and not only of the coeff.
882 'product_uom_qty': product_uos_qty / product.uos_coeff,
883 'th_weight': product_uos_qty / product.uos_coeff * product.weight
885 except ZeroDivisionError:
887 return {'value': value}
889 def copy_data(self, cr, uid, id, default=None, context={}):
892 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
893 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
895 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
896 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
897 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
899 raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sale form !\nPlease set one customer before choosing a product.'))
901 product_uom_obj = self.pool.get('product.uom')
902 partner_obj = self.pool.get('res.partner')
903 product_obj = self.pool.get('product.product')
905 lang = partner_obj.browse(cr, uid, partner_id).lang
906 context = {'lang': lang, 'partner_id': partner_id}
909 return {'value': {'th_weight': 0, 'product_packaging': False,
910 'product_uos_qty': qty}, 'domain': {'product_uom': [],
914 date_order = time.strftime('%Y-%m-%d')
917 product_obj = product_obj.browse(cr, uid, product, context=context)
918 if not packaging and product_obj.packaging:
919 packaging = product_obj.packaging[0].id
920 result['product_packaging'] = packaging
923 default_uom = product_obj.uom_id and product_obj.uom_id.id
924 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
925 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
926 # qty = qty - qty % q + q
927 if qty and (q and not (qty % q) == 0):
931 warn_msg = _("You selected a quantity of %d Units.\nBut it's not compatible with the selected packaging.\nHere is a proposition of quantities according to the packaging: ") % (qty)
932 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
934 'title': _('Packing Information !'),
937 result['product_uom_qty'] = qty
940 uom2 = product_uom_obj.browse(cr, uid, uom)
941 if product_obj.uom_id.category_id.id != uom2.category_id.id:
945 if product_obj.uos_id:
946 uos2 = product_uom_obj.browse(cr, uid, uos)
947 if product_obj.uos_id.category_id.id != uos2.category_id.id:
951 result.update({'type': product_obj.procure_method})
952 if product_obj.description_sale:
953 result['notes'] = product_obj.description_sale
954 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
955 if update_tax: #The quantity only have changed
956 result['delay'] = (product_obj.sale_delay or 0.0)
957 partner = partner_obj.browse(cr, uid, partner_id)
958 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
960 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id])[0][1]
962 if (not uom) and (not uos):
963 result['product_uom'] = product_obj.uom_id.id
964 if product_obj.uos_id:
965 result['product_uos'] = product_obj.uos_id.id
966 result['product_uos_qty'] = qty * product_obj.uos_coeff
967 uos_category_id = product_obj.uos_id.category_id.id
969 result['product_uos'] = False
970 result['product_uos_qty'] = qty
971 uos_category_id = False
972 result['th_weight'] = qty * product_obj.weight
973 domain = {'product_uom':
974 [('category_id', '=', product_obj.uom_id.category_id.id)],
976 [('category_id', '=', uos_category_id)]}
978 elif uos: # only happens if uom is False
979 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
980 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
981 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
982 elif uom: # whether uos is set or not
983 default_uom = product_obj.uom_id and product_obj.uom_id.id
984 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
985 if product_obj.uos_id:
986 result['product_uos'] = product_obj.uos_id.id
987 result['product_uos_qty'] = qty * product_obj.uos_coeff
989 result['product_uos'] = False
990 result['product_uos_qty'] = qty
991 result['th_weight'] = q * product_obj.weight # Round the quantity up
997 'title': 'No Pricelist !',
999 'You have to select a pricelist in the sale form !\n'
1000 'Please set one before choosing a product.'
1003 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1004 product, qty or 1.0, partner_id, {
1010 'title': 'No valid pricelist line found !',
1012 "Couldn't find a pricelist line matching this product and quantity.\n"
1013 "You have to change either the product, the quantity or the pricelist."
1016 result.update({'price_unit': price})
1017 return {'value': result, 'domain': domain, 'warning': warning}
1019 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1020 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1021 lang=False, update_tax=True, date_order=False):
1022 res = self.product_id_change(cursor, user, ids, pricelist, product,
1023 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1024 partner_id=partner_id, lang=lang, update_tax=update_tax,
1025 date_order=date_order)
1026 if 'product_uom' in res['value']:
1027 del res['value']['product_uom']
1029 res['value']['price_unit'] = 0.0
1032 def unlink(self, cr, uid, ids, context={}):
1033 """Allows to delete sale order lines in draft,cancel states"""
1034 for rec in self.browse(cr, uid, ids, context=context):
1035 if rec.state not in ['draft', 'cancel']:
1036 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1037 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1042 class sale_config_picking_policy(osv.osv_memory):
1043 _name = 'sale.config.picking_policy'
1045 'name': fields.char('Name', size=64),
1046 'picking_policy': fields.selection([
1047 ('direct', 'Direct Delivery'),
1048 ('one', 'All at Once')
1049 ], 'Packing Default Policy', required=True),
1050 'order_policy': fields.selection([
1051 ('manual', 'Invoice Based on Sales Orders'),
1052 ('picking', 'Invoice Based on Deliveries'),
1053 ], 'Shipping Default Policy', required=True),
1054 'step': fields.selection([
1055 ('one', 'Delivery Order Only'),
1056 ('two', 'Packing List & Delivery Order')
1057 ], 'Steps To Deliver a Sale Order', required=True,
1058 help="By default, Open ERP is able to manage complex routing and paths "\
1059 "of products in your warehouse and partner locations. This will configure "\
1060 "the most common and simple methods to deliver products to the customer "\
1061 "in one or two operations by the worker.")
1064 'picking_policy': lambda *a: 'direct',
1065 'order_policy': lambda *a: 'picking',
1066 'step': lambda *a: 'one'
1069 def set_default(self, cr, uid, ids, context=None):
1070 for o in self.browse(cr, uid, ids, context=context):
1071 ir_values_obj = self.pool.get('ir.values')
1072 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1073 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1076 md = self.pool.get('ir.model.data')
1077 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1078 group_id = md.browse(cr, uid, group_id, context).res_id
1079 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1080 menu_id = md.browse(cr, uid, menu_id, context).res_id
1081 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1083 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1084 location_id = md.browse(cr, uid, location_id, context).res_id
1085 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1088 'view_type': 'form',
1089 "view_mode": 'form',
1090 'res_model': 'ir.actions.configuration.wizard',
1091 'type': 'ir.actions.act_window',
1095 def action_cancel(self, cr, uid, ids, context=None):
1097 'view_type': 'form',
1098 "view_mode": 'form',
1099 'res_model': 'ir.actions.configuration.wizard',
1100 'type': 'ir.actions.act_window',
1104 sale_config_picking_policy()