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', 'Invoices', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
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, digits=(16, int(config['price_accuracy'])), 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, digits=(16, int(config['price_accuracy'])), 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, digits=(16, int(config['price_accuracy'])), 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
384 inv_obj = self.pool.get('account.invoice')
385 inv.update(self._inv_get(cr, uid, order))
386 inv_id = inv_obj.create(cr, uid, inv)
387 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
388 if data.get('value', False):
389 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
390 inv_obj.button_compute(cr, uid, [inv_id])
393 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception']):
398 for o in self.browse(cr, uid, ids):
400 for line in o.order_line:
401 if (line.state in states) and not line.invoiced:
402 lines.append(line.id)
403 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
405 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
408 for o in self.browse(cr, uid, ids):
409 for i in o.invoice_ids:
410 if i.state == 'draft':
412 picking_obj = self.pool.get('stock.picking')
413 for val in invoices.values():
415 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []))
417 self.write(cr, uid, [o.id], {'state': 'progress'})
418 if o.order_policy == 'picking':
419 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
420 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
422 for order, il in val:
423 res = self._make_invoice(cr, uid, order, il)
424 invoice_ids.append(res)
425 self.write(cr, uid, [order.id], {'state': 'progress'})
426 if order.order_policy == 'picking':
427 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
428 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
431 def action_invoice_cancel(self, cr, uid, ids, context={}):
432 for sale in self.browse(cr, uid, ids):
433 for line in sale.order_line:
435 for iline in line.invoice_lines:
436 if iline.invoice_id and iline.invoice_id.state == 'cancel':
440 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
441 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
444 def action_cancel(self, cr, uid, ids, context={}):
446 sale_order_line_obj = self.pool.get('sale.order.line')
447 for sale in self.browse(cr, uid, ids):
448 for pick in sale.picking_ids:
449 if pick.state not in ('draft', 'cancel'):
450 raise osv.except_osv(
451 _('Could not cancel sale order !'),
452 _('You must first cancel all packing attached to this sale order.'))
453 for r in self.read(cr, uid, ids, ['picking_ids']):
454 for pick in r['picking_ids']:
455 wf_service = netsvc.LocalService("workflow")
456 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
457 for inv in sale.invoice_ids:
458 if inv.state not in ('draft', 'cancel'):
459 raise osv.except_osv(
460 _('Could not cancel this sale order !'),
461 _('You must first cancel all invoices attached to this sale order.'))
462 for r in self.read(cr, uid, ids, ['invoice_ids']):
463 for inv in r['invoice_ids']:
464 wf_service = netsvc.LocalService("workflow")
465 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
466 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
468 self.write(cr, uid, ids, {'state': 'cancel'})
471 def action_wait(self, cr, uid, ids, *args):
472 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
473 event_obj = self.pool.get('res.partner.event')
474 for o in self.browse(cr, uid, ids):
476 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
477 'partner_id': o.partner_id.id,\
478 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
479 'user_id': (o.user_id and o.user_id.id) or uid,\
480 'partner_type': 'customer', 'probability': 1.0,\
481 'planned_revenue': o.amount_untaxed})
482 if (o.order_policy == 'manual'):
483 self.write(cr, uid, [o.id], {'state': 'manual'})
485 self.write(cr, uid, [o.id], {'state': 'progress'})
486 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
488 def procurement_lines_get(self, cr, uid, ids, *args):
490 for order in self.browse(cr, uid, ids, context={}):
491 for line in order.order_line:
492 if line.procurement_id:
493 res.append(line.procurement_id.id)
496 # if mode == 'finished':
497 # returns True if all lines are done, False otherwise
498 # if mode == 'canceled':
499 # returns True if there is at least one canceled line, False otherwise
500 def test_state(self, cr, uid, ids, mode, *args):
501 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
506 write_cancel_ids = []
507 stock_move_obj = self.pool.get('stock.move')
508 for order in self.browse(cr, uid, ids, context={}):
510 #check for pending deliveries
511 pending_deliveries = False
512 # check => if order_lines do not exist,don't proceed for any mode.
513 if not order.order_line:
515 for line in order.order_line:
516 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
517 for move in stock_move_obj.browse( cr, uid, move_ids ):
518 #if one of the related order lines is in state draft, auto or confirmed
519 #this order line is not yet delivered
520 if move.state in ('draft', 'auto', 'confirmed'):
521 pending_deliveries = True
522 # Reason => if there are no move lines,the following condition will always set to be true,and will set SO to 'DONE'.
523 # Added move_ids check to SOLVE.
524 if move_ids and ((not line.procurement_id) or (line.procurement_id.state=='done')) and not pending_deliveries:
526 if line.state != 'done':
527 write_done_ids.append(line.id)
530 if line.procurement_id:
531 if (line.procurement_id.state == 'cancel'):
533 if line.state != 'exception':
534 write_cancel_ids.append(line.id)
538 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
540 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
542 if mode == 'finished':
544 elif mode == 'canceled':
550 def action_ship_create(self, cr, uid, ids, *args):
552 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
553 for order in self.browse(cr, uid, ids, context={}):
554 output_id = order.shop_id.warehouse_id.lot_output_id.id
556 for line in order.order_line:
558 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
559 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
560 if line.state == 'done':
562 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
563 location_id = order.shop_id.warehouse_id.lot_stock_id.id
565 loc_dest_id = order.partner_id.property_stock_customer.id
566 picking_id = self.pool.get('stock.picking').create(cr, uid, {
567 'origin': order.name,
570 'move_type': order.picking_policy,
572 'address_id': order.partner_shipping_id.id,
574 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
578 move_id = self.pool.get('stock.move').create(cr, uid, {
579 'name': line.name[:64],
580 'picking_id': picking_id,
581 'product_id': line.product_id.id,
582 'date_planned': date_planned,
583 'product_qty': line.product_uom_qty,
584 'product_uom': line.product_uom.id,
585 'product_uos_qty': line.product_uos_qty,
586 'product_uos': (line.product_uos and line.product_uos.id)\
587 or line.product_uom.id,
588 'product_packaging': line.product_packaging.id,
589 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
590 'location_id': location_id,
591 'location_dest_id': output_id,
592 'sale_line_id': line.id,
593 'tracking_id': False,
598 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
600 'origin': order.name,
601 'date_planned': date_planned,
602 'product_id': line.product_id.id,
603 'product_qty': line.product_uom_qty,
604 'product_uom': line.product_uom.id,
605 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
606 or line.product_uom_qty,
607 'product_uos': (line.product_uos and line.product_uos.id)\
608 or line.product_uom.id,
609 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
610 'procure_method': line.type,
612 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
614 wf_service = netsvc.LocalService("workflow")
615 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
616 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
617 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
618 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
620 'origin': order.name,
621 'date_planned': date_planned,
622 'product_id': line.product_id.id,
623 'product_qty': line.product_uom_qty,
624 'product_uom': line.product_uom.id,
625 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
626 'procure_method': line.type,
627 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
629 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
630 wf_service = netsvc.LocalService("workflow")
631 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
634 # No procurement because no product in the sale.order.line.
640 wf_service = netsvc.LocalService("workflow")
641 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
643 if order.state == 'shipping_except':
644 val['state'] = 'progress'
646 if (order.order_policy == 'manual'):
647 for line in order.order_line:
648 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
649 val['state'] = 'manual'
651 self.write(cr, uid, [order.id], val)
655 def action_ship_end(self, cr, uid, ids, context={}):
656 for order in self.browse(cr, uid, ids):
657 val = {'shipped': True}
658 if order.state == 'shipping_except':
659 val['state'] = 'progress'
660 if (order.order_policy == 'manual'):
661 for line in order.order_line:
662 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
663 val['state'] = 'manual'
665 for line in order.order_line:
667 if line.state == 'exception':
668 towrite.append(line.id)
670 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
671 self.write(cr, uid, [order.id], val)
674 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
675 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
677 part = inv['partner_id'] and inv['partner_id'][0]
678 pr = inv['amount_untaxed'] or 0.0
679 partnertype = 'customer'
682 'name': 'Order: '+name,
684 'description': 'Order '+str(inv['id']),
687 'date': time.strftime('%Y-%m-%d'),
690 'partner_type': partnertype,
692 'planned_revenue': pr,
696 self.pool.get('res.partner.event').create(cr, uid, event)
698 def has_stockable_products(self, cr, uid, ids, *args):
699 for order in self.browse(cr, uid, ids):
700 for order_line in order.order_line:
701 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
706 # TODO add a field price_unit_uos
707 # - update it on change product and unit price
708 # - use it in report if there is a uos
709 class sale_order_line(osv.osv):
710 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
712 for line in self.browse(cr, uid, ids):
713 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
716 def _amount_line(self, cr, uid, ids, field_name, arg, context):
718 cur_obj = self.pool.get('res.currency')
719 for line in self.browse(cr, uid, ids):
720 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
721 cur = line.order_id.pricelist_id.currency_id
722 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
725 def _number_packages(self, cr, uid, ids, field_name, arg, context):
727 for line in self.browse(cr, uid, ids):
729 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
734 _name = 'sale.order.line'
735 _description = 'Sale Order line'
737 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
738 'name': fields.char('Description', size=256, required=True, select=True),
739 'sequence': fields.integer('Sequence'),
740 '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"),
741 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
742 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
743 'invoiced': fields.boolean('Invoiced', readonly=True),
744 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
745 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
746 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
747 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
748 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
749 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True),
750 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
751 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
752 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
753 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
754 'product_uos_qty': fields.float('Quantity (UoS)'),
755 'product_uos': fields.many2one('product.uom', 'Product UoS'),
756 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
757 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
758 'discount': fields.float('Discount (%)', digits=(16, 2)),
759 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
760 'notes': fields.text('Notes'),
761 'th_weight': fields.float('Weight'),
762 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
763 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
765 _order = 'sequence, id'
767 'discount': lambda *a: 0.0,
768 'delay': lambda *a: 0.0,
769 'product_uom_qty': lambda *a: 1,
770 'product_uos_qty': lambda *a: 1,
771 'sequence': lambda *a: 10,
772 'invoiced': lambda *a: 0,
773 'state': lambda *a: 'draft',
774 'type': lambda *a: 'make_to_stock',
775 'product_packaging': lambda *a: False
778 def invoice_line_create(self, cr, uid, ids, context={}):
779 def _get_line_qty(line):
780 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
782 return line.product_uos_qty or 0.0
783 return line.product_uom_qty
785 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
786 line.procurement_id.id, context)
788 def _get_line_uom(line):
789 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
791 return line.product_uos.id
792 return line.product_uom.id
794 return self.pool.get('mrp.procurement').uom_get(cr, uid,
795 line.procurement_id.id, context)
799 for line in self.browse(cr, uid, ids, context):
800 if not line.invoiced:
802 a = line.product_id.product_tmpl_id.property_account_income.id
804 a = line.product_id.categ_id.property_account_income_categ.id
806 raise osv.except_osv(_('Error !'),
807 _('There is no income account defined ' \
808 'for this product: "%s" (id:%d)') % \
809 (line.product_id.name, line.product_id.id,))
811 a = self.pool.get('ir.property').get(cr, uid,
812 'property_account_income_categ', 'product.category',
814 uosqty = _get_line_qty(line)
815 uos_id = _get_line_uom(line)
818 pu = round(line.price_unit * line.product_uom_qty / uosqty,
819 int(config['price_accuracy']))
820 fpos = line.order_id.fiscal_position or False
821 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
823 raise osv.except_osv(_('Error !'),
824 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
825 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
827 'origin': line.order_id.name,
831 'discount': line.discount,
833 'product_id': line.product_id.id or False,
834 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
836 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
838 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
839 self.write(cr, uid, [line.id], {'invoiced': True})
841 sales[line.order_id.id] = True
842 create_ids.append(inv_id)
844 # Trigger workflow events
845 wf_service = netsvc.LocalService("workflow")
846 for sid in sales.keys():
847 wf_service.trg_write(uid, 'sale.order', sid, cr)
850 def button_cancel(self, cr, uid, ids, context={}):
851 for line in self.browse(cr, uid, ids, context=context):
853 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
854 return self.write(cr, uid, ids, {'state': 'cancel'})
856 def button_confirm(self, cr, uid, ids, context={}):
857 return self.write(cr, uid, ids, {'state': 'confirmed'})
859 def button_done(self, cr, uid, ids, context={}):
860 wf_service = netsvc.LocalService("workflow")
861 res = self.write(cr, uid, ids, {'state': 'done'})
862 for line in self.browse(cr, uid, ids, context):
863 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
867 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
868 product_obj = self.pool.get('product.product')
870 return {'value': {'product_uom': product_uos,
871 'product_uom_qty': product_uos_qty}, 'domain': {}}
873 product = product_obj.browse(cr, uid, product_id)
875 'product_uom': product.uom_id.id,
877 # FIXME must depend on uos/uom of the product and not only of the coeff.
880 'product_uom_qty': product_uos_qty / product.uos_coeff,
881 'th_weight': product_uos_qty / product.uos_coeff * product.weight
883 except ZeroDivisionError:
885 return {'value': value}
887 def copy_data(self, cr, uid, id, default=None, context={}):
890 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
891 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
893 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
894 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
895 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
897 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.'))
899 product_uom_obj = self.pool.get('product.uom')
900 partner_obj = self.pool.get('res.partner')
901 product_obj = self.pool.get('product.product')
903 lang = partner_obj.browse(cr, uid, partner_id).lang
904 context = {'lang': lang, 'partner_id': partner_id}
907 return {'value': {'th_weight': 0, 'product_packaging': False,
908 'product_uos_qty': qty}, 'domain': {'product_uom': [],
912 date_order = time.strftime('%Y-%m-%d')
915 product_obj = product_obj.browse(cr, uid, product, context=context)
916 if not packaging and product_obj.packaging:
917 packaging = product_obj.packaging[0].id
918 result['product_packaging'] = packaging
921 default_uom = product_obj.uom_id and product_obj.uom_id.id
922 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
923 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
924 # qty = qty - qty % q + q
925 if qty and (q and not (qty % q) == 0):
929 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)
930 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
932 'title': _('Packing Information !'),
935 result['product_uom_qty'] = qty
938 uom2 = product_uom_obj.browse(cr, uid, uom)
939 if product_obj.uom_id.category_id.id != uom2.category_id.id:
943 if product_obj.uos_id:
944 uos2 = product_uom_obj.browse(cr, uid, uos)
945 if product_obj.uos_id.category_id.id != uos2.category_id.id:
949 result.update({'type': product_obj.procure_method})
950 if product_obj.description_sale:
951 result['notes'] = product_obj.description_sale
952 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
953 if update_tax: #The quantity only have changed
954 result['delay'] = (product_obj.sale_delay or 0.0)
955 partner = partner_obj.browse(cr, uid, partner_id)
956 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
958 result['name'] = product_obj.partner_ref
960 if (not uom) and (not uos):
961 result['product_uom'] = product_obj.uom_id.id
962 if product_obj.uos_id:
963 result['product_uos'] = product_obj.uos_id.id
964 result['product_uos_qty'] = qty * product_obj.uos_coeff
965 uos_category_id = product_obj.uos_id.category_id.id
967 result['product_uos'] = False
968 result['product_uos_qty'] = qty
969 uos_category_id = False
970 result['th_weight'] = qty * product_obj.weight
971 domain = {'product_uom':
972 [('category_id', '=', product_obj.uom_id.category_id.id)],
974 [('category_id', '=', uos_category_id)]}
976 elif uos: # only happens if uom is False
977 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
978 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
979 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
980 elif uom: # whether uos is set or not
981 default_uom = product_obj.uom_id and product_obj.uom_id.id
982 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
983 if product_obj.uos_id:
984 result['product_uos'] = product_obj.uos_id.id
985 result['product_uos_qty'] = qty * product_obj.uos_coeff
987 result['product_uos'] = False
988 result['product_uos_qty'] = qty
989 result['th_weight'] = q * product_obj.weight # Round the quantity up
995 'title': 'No Pricelist !',
997 'You have to select a pricelist in the sale form !\n'
998 'Please set one before choosing a product.'
1001 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1002 product, qty or 1.0, partner_id, {
1008 'title': 'No valid pricelist line found !',
1010 "Couldn't find a pricelist line matching this product and quantity.\n"
1011 "You have to change either the product, the quantity or the pricelist."
1014 result.update({'price_unit': price})
1015 return {'value': result, 'domain': domain, 'warning': warning}
1017 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1018 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1019 lang=False, update_tax=True, date_order=False):
1020 res = self.product_id_change(cursor, user, ids, pricelist, product,
1021 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1022 partner_id=partner_id, lang=lang, update_tax=update_tax,
1023 date_order=date_order)
1024 if 'product_uom' in res['value']:
1025 del res['value']['product_uom']
1027 res['value']['price_unit'] = 0.0
1030 def unlink(self, cr, uid, ids, context={}):
1031 """Allows to delete sale order lines in draft,cancel states"""
1032 for rec in self.browse(cr, uid, ids, context=context):
1033 if rec.state not in ['draft', 'cancel']:
1034 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1035 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1040 class sale_config_picking_policy(osv.osv_memory):
1041 _name = 'sale.config.picking_policy'
1043 'name': fields.char('Name', size=64),
1044 'picking_policy': fields.selection([
1045 ('direct', 'Direct Delivery'),
1046 ('one', 'All at Once')
1047 ], 'Packing Default Policy', required=True),
1048 'order_policy': fields.selection([
1049 ('manual', 'Invoice Based on Sales Orders'),
1050 ('picking', 'Invoice Based on Deliveries'),
1051 ], 'Shipping Default Policy', required=True),
1052 'step': fields.selection([
1053 ('one', 'Delivery Order Only'),
1054 ('two', 'Packing List & Delivery Order')
1055 ], 'Steps To Deliver a Sale Order', required=True,
1056 help="By default, Open ERP is able to manage complex routing and paths "\
1057 "of products in your warehouse and partner locations. This will configure "\
1058 "the most common and simple methods to deliver products to the customer "\
1059 "in one or two operations by the worker.")
1062 'picking_policy': lambda *a: 'direct',
1063 'order_policy': lambda *a: 'picking',
1064 'step': lambda *a: 'one'
1067 def set_default(self, cr, uid, ids, context=None):
1068 for o in self.browse(cr, uid, ids, context=context):
1069 ir_values_obj = self.pool.get('ir.values')
1070 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1071 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1074 md = self.pool.get('ir.model.data')
1075 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1076 group_id = md.browse(cr, uid, group_id, context).res_id
1077 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1078 menu_id = md.browse(cr, uid, menu_id, context).res_id
1079 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1081 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1082 location_id = md.browse(cr, uid, location_id, context).res_id
1083 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1086 'view_type': 'form',
1087 "view_mode": 'form',
1088 'res_model': 'ir.actions.configuration.wizard',
1089 'type': 'ir.actions.act_window',
1093 def action_cancel(self, cr, uid, ids, context=None):
1095 'view_type': 'form',
1096 "view_mode": 'form',
1097 'res_model': 'ir.actions.configuration.wizard',
1098 'type': 'ir.actions.act_window',
1102 sale_config_picking_policy()