1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 from osv import fields, osv
25 from mx import DateTime
26 from tools import config
27 from tools.translate import _
30 class sale_shop(osv.osv):
32 _description = "Sale Shop"
34 'name': fields.char('Shop Name', size=64, required=True),
35 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
36 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
37 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
38 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
43 def _incoterm_get(self, cr, uid, context={}):
44 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
48 class sale_order(osv.osv):
50 _description = "Sale Order"
52 def copy(self, cr, uid, id, default=None, context={}):
60 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
62 return super(sale_order, self).copy(cr, uid, id, default, context)
64 def _amount_line_tax(self, cr, uid, line, context={}):
66 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id):
70 def _amount_all(self, cr, uid, ids, field_name, arg, context):
72 cur_obj = self.pool.get('res.currency')
73 for order in self.browse(cr, uid, ids):
75 'amount_untaxed': 0.0,
80 cur = order.pricelist_id.currency_id
81 for line in order.order_line:
82 val1 += line.price_subtotal
83 val += self._amount_line_tax(cr, uid, line, context)
84 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
85 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
86 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
89 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
96 p.sale_id,sum(m.product_qty), m.state
100 stock_picking p on (p.id=m.picking_id)
102 p.sale_id in ('''+','.join(map(str, ids))+''')
103 GROUP BY m.state, p.sale_id''')
104 for oid, nbr, state in cr.fetchall():
105 if state == 'cancel':
108 res[oid][0] += nbr or 0.0
109 res[oid][1] += nbr or 0.0
111 res[oid][1] += nbr or 0.0
116 res[r] = 100.0 * res[r][0] / res[r][1]
117 for order in self.browse(cr, uid, ids, context):
119 res[order.id] = 100.0
122 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
124 for sale in self.browse(cursor, user, ids, context=context):
129 for invoice in sale.invoice_ids:
130 if invoice.state not in ('draft', 'cancel'):
131 tot += invoice.amount_untaxed
134 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
139 def _invoiced(self, cursor, user, ids, name, arg, context=None):
141 for sale in self.browse(cursor, user, ids, context=context):
143 for invoice in sale.invoice_ids:
144 if invoice.state != 'paid':
147 if not sale.invoice_ids:
151 def _invoiced_search(self, cursor, user, obj, name, args):
159 clause += 'AND inv.state = \'paid\''
161 clause += 'AND inv.state <> \'paid\''
163 cursor.execute('SELECT rel.order_id ' \
164 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
165 'WHERE rel.invoice_id = inv.id ' + clause)
166 res = cursor.fetchall()
168 cursor.execute('SELECT sale.id ' \
169 'FROM sale_order AS sale ' \
170 'WHERE sale.id NOT IN ' \
171 '(SELECT rel.order_id ' \
172 'FROM sale_order_invoice_rel AS rel)')
173 res.extend(cursor.fetchall())
175 return [('id', '=', 0)]
176 return [('id', 'in', [x[0] for x in res])]
178 def _get_order(self, cr, uid, ids, context={}):
180 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
181 result[line.order_id.id] = True
185 'name': fields.char('Order Reference', size=64, required=True, select=True),
186 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
187 'origin': fields.char('Origin', size=64),
188 'client_order_ref': fields.char('Customer Ref', size=64),
190 'state': fields.selection([
191 ('draft', 'Quotation'),
192 ('waiting_date', 'Waiting Schedule'),
193 ('manual', 'Manual In Progress'),
194 ('progress', 'In Progress'),
195 ('shipping_except', 'Shipping Exception'),
196 ('invoice_except', 'Invoice Exception'),
198 ('cancel', 'Cancelled')
199 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Date Ordered'.", select=True),
200 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
202 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
203 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
204 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
205 'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact that requested the order or quotation."),
206 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
208 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
209 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
210 'Picking Policy', required=True, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
211 'order_policy': fields.selection([
212 ('prepaid', 'Payment Before Delivery'),
213 ('manual', 'Shipping & Manual Invoice'),
214 ('postpaid', 'Invoice on Order After Delivery'),
215 ('picking', 'Invoice from Picking'),
216 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
217 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
218 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
219 - The 'Shipping & Manual Invoice' will create the packing order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
220 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
221 - The 'Invoice from the picking' choice is used to create an invoice during the packing process."""),
222 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
225 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
226 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
227 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
228 'shipped': fields.boolean('Picked', readonly=True),
229 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
230 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
231 'invoiced': fields.function(_invoiced, method=True, string='Paid',
232 fnct_search=_invoiced_search, type='boolean'),
233 'note': fields.text('Notes'),
235 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
241 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
243 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
244 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
247 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
249 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
250 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True),
255 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
256 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
257 'company_id': fields.many2one('res.company','Company'),
260 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', c),
261 'picking_policy': lambda *a: 'direct',
262 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
263 'order_policy': lambda *a: 'manual',
264 'state': lambda *a: 'draft',
265 'user_id': lambda obj, cr, uid, context: uid,
266 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
267 'invoice_quantity': lambda *a: 'order',
268 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
269 'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
270 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
271 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist.id,
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),
384 'company_id' : order.company_id.id,
386 inv_obj = self.pool.get('account.invoice')
387 inv.update(self._inv_get(cr, uid, order))
388 inv_id = inv_obj.create(cr, uid, inv)
389 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
390 if data.get('value', False):
391 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
392 inv_obj.button_compute(cr, uid, [inv_id])
395 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
401 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
402 # last day of the last month as invoice date
404 context['date_inv'] = date_inv
405 for o in self.browse(cr, uid, ids):
407 for line in o.order_line:
408 if (line.state in states) and not line.invoiced:
409 lines.append(line.id)
410 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
412 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
415 for o in self.browse(cr, uid, ids):
416 for i in o.invoice_ids:
417 if i.state == 'draft':
419 picking_obj = self.pool.get('stock.picking')
420 for val in invoices.values():
422 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
424 self.write(cr, uid, [o.id], {'state': 'progress'})
425 if o.order_policy == 'picking':
426 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
427 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
429 for order, il in val:
430 res = self._make_invoice(cr, uid, order, il, context=context)
431 invoice_ids.append(res)
432 self.write(cr, uid, [order.id], {'state': 'progress'})
433 if order.order_policy == 'picking':
434 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
435 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
438 def action_invoice_cancel(self, cr, uid, ids, context={}):
439 for sale in self.browse(cr, uid, ids):
440 for line in sale.order_line:
442 for iline in line.invoice_lines:
443 if iline.invoice_id and iline.invoice_id.state == 'cancel':
447 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
448 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
451 def action_cancel(self, cr, uid, ids, context={}):
453 sale_order_line_obj = self.pool.get('sale.order.line')
454 for sale in self.browse(cr, uid, ids):
455 for pick in sale.picking_ids:
456 if pick.state not in ('draft', 'cancel'):
457 raise osv.except_osv(
458 _('Could not cancel sale order !'),
459 _('You must first cancel all packing attached to this sale order.'))
460 for r in self.read(cr, uid, ids, ['picking_ids']):
461 for pick in r['picking_ids']:
462 wf_service = netsvc.LocalService("workflow")
463 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
464 for inv in sale.invoice_ids:
465 if inv.state not in ('draft', 'cancel'):
466 raise osv.except_osv(
467 _('Could not cancel this sale order !'),
468 _('You must first cancel all invoices attached to this sale order.'))
469 for r in self.read(cr, uid, ids, ['invoice_ids']):
470 for inv in r['invoice_ids']:
471 wf_service = netsvc.LocalService("workflow")
472 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
473 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
475 self.write(cr, uid, ids, {'state': 'cancel'})
478 def action_wait(self, cr, uid, ids, *args):
479 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
480 event_obj = self.pool.get('res.partner.event')
481 for o in self.browse(cr, uid, ids):
483 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
484 'partner_id': o.partner_id.id,\
485 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
486 'user_id': (o.user_id and o.user_id.id) or uid,\
487 'partner_type': 'customer', 'probability': 1.0,\
488 'planned_revenue': o.amount_untaxed})
489 if (o.order_policy == 'manual'):
490 self.write(cr, uid, [o.id], {'state': 'manual'})
492 self.write(cr, uid, [o.id], {'state': 'progress'})
493 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
495 def procurement_lines_get(self, cr, uid, ids, *args):
497 for order in self.browse(cr, uid, ids, context={}):
498 for line in order.order_line:
499 if line.procurement_id:
500 res.append(line.procurement_id.id)
503 # if mode == 'finished':
504 # returns True if all lines are done, False otherwise
505 # if mode == 'canceled':
506 # returns True if there is at least one canceled line, False otherwise
507 def test_state(self, cr, uid, ids, mode, *args):
508 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
513 write_cancel_ids = []
514 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
519 # check => if order_lines do not exist,don't proceed for any mode.
520 if not order.order_line:
522 for line in order.order_line:
523 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
524 for move in stock_move_obj.browse( cr, uid, move_ids ):
525 #if one of the related order lines is in state draft, auto or confirmed
526 #this order line is not yet delivered
527 if move.state in ('draft', 'auto', 'confirmed'):
528 pending_deliveries = True
529 # Reason => if there are no move lines,the following condition will always set to be true,and will set SO to 'DONE'.
530 # Added move_ids check to SOLVE.
531 if move_ids and ((not line.procurement_id) or (line.procurement_id.state=='done')) and not pending_deliveries:
533 if line.state != 'done':
534 write_done_ids.append(line.id)
537 if line.procurement_id:
538 if (line.procurement_id.state == 'cancel'):
540 if line.state != 'exception':
541 write_cancel_ids.append(line.id)
545 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
547 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
549 if mode == 'finished':
551 elif mode == 'canceled':
557 def action_ship_create(self, cr, uid, ids, *args):
559 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
560 for order in self.browse(cr, uid, ids, context={}):
561 output_id = order.shop_id.warehouse_id.lot_output_id.id
563 for line in order.order_line:
565 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
566 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
567 if line.state == 'done':
569 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
570 location_id = order.shop_id.warehouse_id.lot_stock_id.id
572 loc_dest_id = order.partner_id.property_stock_customer.id
573 picking_id = self.pool.get('stock.picking').create(cr, uid, {
574 'origin': order.name,
577 'move_type': order.picking_policy,
579 'address_id': order.partner_shipping_id.id,
581 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
582 'company_id': order.company_id.id,
585 move_id = self.pool.get('stock.move').create(cr, uid, {
586 'name': line.name[:64],
587 'picking_id': picking_id,
588 'product_id': line.product_id.id,
589 'date_planned': date_planned,
590 'product_qty': line.product_uom_qty,
591 'product_uom': line.product_uom.id,
592 'product_uos_qty': line.product_uos_qty,
593 'product_uos': (line.product_uos and line.product_uos.id)\
594 or line.product_uom.id,
595 'product_packaging': line.product_packaging.id,
596 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
597 'location_id': location_id,
598 'location_dest_id': output_id,
599 'sale_line_id': line.id,
600 'tracking_id': False,
604 'company_id': order.company_id.id,
606 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
608 'origin': order.name,
609 'date_planned': date_planned,
610 'product_id': line.product_id.id,
611 'product_qty': line.product_uom_qty,
612 'product_uom': line.product_uom.id,
613 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
614 or line.product_uom_qty,
615 'product_uos': (line.product_uos and line.product_uos.id)\
616 or line.product_uom.id,
617 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
618 'procure_method': line.type,
620 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
621 'company_id': order.company_id.id,
623 wf_service = netsvc.LocalService("workflow")
624 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
625 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
626 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
627 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
629 'origin': order.name,
630 'date_planned': date_planned,
631 'product_id': line.product_id.id,
632 'product_qty': line.product_uom_qty,
633 'product_uom': line.product_uom.id,
634 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
635 'procure_method': line.type,
636 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
637 'company_id': order.company_id.id,
639 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
640 wf_service = netsvc.LocalService("workflow")
641 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
644 # No procurement because no product in the sale.order.line.
650 wf_service = netsvc.LocalService("workflow")
651 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
653 if order.state == 'shipping_except':
654 val['state'] = 'progress'
656 if (order.order_policy == 'manual'):
657 for line in order.order_line:
658 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
659 val['state'] = 'manual'
661 self.write(cr, uid, [order.id], val)
665 def action_ship_end(self, cr, uid, ids, context={}):
666 for order in self.browse(cr, uid, ids):
667 val = {'shipped': True}
668 if order.state == 'shipping_except':
669 val['state'] = 'progress'
670 if (order.order_policy == 'manual'):
671 for line in order.order_line:
672 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
673 val['state'] = 'manual'
675 for line in order.order_line:
677 if line.state == 'exception':
678 towrite.append(line.id)
680 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
681 self.write(cr, uid, [order.id], val)
684 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
685 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
687 part = inv['partner_id'] and inv['partner_id'][0]
688 pr = inv['amount_untaxed'] or 0.0
689 partnertype = 'customer'
692 'name': 'Order: '+name,
694 'description': 'Order '+str(inv['id']),
697 'date': time.strftime('%Y-%m-%d'),
700 'partner_type': partnertype,
702 'planned_revenue': pr,
706 self.pool.get('res.partner.event').create(cr, uid, event)
708 def has_stockable_products(self, cr, uid, ids, *args):
709 for order in self.browse(cr, uid, ids):
710 for order_line in order.order_line:
711 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
716 # TODO add a field price_unit_uos
717 # - update it on change product and unit price
718 # - use it in report if there is a uos
719 class sale_order_line(osv.osv):
720 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
722 for line in self.browse(cr, uid, ids):
723 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
726 def _amount_line(self, cr, uid, ids, field_name, arg, context):
728 cur_obj = self.pool.get('res.currency')
729 for line in self.browse(cr, uid, ids):
730 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
731 cur = line.order_id.pricelist_id.currency_id
732 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
735 def _number_packages(self, cr, uid, ids, field_name, arg, context):
737 for line in self.browse(cr, uid, ids):
739 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
744 _name = 'sale.order.line'
745 _description = 'Sale Order line'
747 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
748 'name': fields.char('Description', size=256, required=True, select=True),
749 'sequence': fields.integer('Sequence'),
750 '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"),
751 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
752 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
753 'invoiced': fields.boolean('Invoiced', readonly=True),
754 'procurement_id': fields.many2one('mrp.procurement', 'Requisition'),
755 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
756 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
757 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
758 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
759 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Requisition Method', required=True),
760 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
761 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
762 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
763 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
764 'product_uos_qty': fields.float('Quantity (UoS)'),
765 'product_uos': fields.many2one('product.uom', 'Product UoS'),
766 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
767 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
768 'discount': fields.float('Discount (%)', digits=(16, 2)),
769 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
770 'notes': fields.text('Notes'),
771 'th_weight': fields.float('Weight'),
772 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'State', required=True, readonly=True),
773 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
774 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
775 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
777 _order = 'sequence, id'
779 'discount': lambda *a: 0.0,
780 'delay': lambda *a: 0.0,
781 'product_uom_qty': lambda *a: 1,
782 'product_uos_qty': lambda *a: 1,
783 'sequence': lambda *a: 10,
784 'invoiced': lambda *a: 0,
785 'state': lambda *a: 'draft',
786 'type': lambda *a: 'make_to_stock',
787 'product_packaging': lambda *a: False
790 def invoice_line_create(self, cr, uid, ids, context={}):
791 def _get_line_qty(line):
792 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
794 return line.product_uos_qty or 0.0
795 return line.product_uom_qty
797 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
798 line.procurement_id.id, context)
800 def _get_line_uom(line):
801 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
803 return line.product_uos.id
804 return line.product_uom.id
806 return self.pool.get('mrp.procurement').uom_get(cr, uid,
807 line.procurement_id.id, context)
811 for line in self.browse(cr, uid, ids, context):
812 if not line.invoiced:
814 a = line.product_id.product_tmpl_id.property_account_income.id
816 a = line.product_id.categ_id.property_account_income_categ.id
818 raise osv.except_osv(_('Error !'),
819 _('There is no income account defined ' \
820 'for this product: "%s" (id:%d)') % \
821 (line.product_id.name, line.product_id.id,))
823 a = self.pool.get('ir.property').get(cr, uid,
824 'property_account_income_categ', 'product.category',
826 uosqty = _get_line_qty(line)
827 uos_id = _get_line_uom(line)
830 pu = round(line.price_unit * line.product_uom_qty / uosqty,
831 int(config['price_accuracy']))
832 fpos = line.order_id.fiscal_position or False
833 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
835 raise osv.except_osv(_('Error !'),
836 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
837 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
839 'origin': line.order_id.name,
843 'discount': line.discount,
845 'product_id': line.product_id.id or False,
846 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
848 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
850 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
851 self.write(cr, uid, [line.id], {'invoiced': True})
853 sales[line.order_id.id] = True
854 create_ids.append(inv_id)
856 # Trigger workflow events
857 wf_service = netsvc.LocalService("workflow")
858 for sid in sales.keys():
859 wf_service.trg_write(uid, 'sale.order', sid, cr)
862 def button_cancel(self, cr, uid, ids, context={}):
863 for line in self.browse(cr, uid, ids, context=context):
865 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
866 return self.write(cr, uid, ids, {'state': 'cancel'})
868 def button_confirm(self, cr, uid, ids, context={}):
869 return self.write(cr, uid, ids, {'state': 'confirmed'})
871 def button_done(self, cr, uid, ids, context={}):
872 wf_service = netsvc.LocalService("workflow")
873 res = self.write(cr, uid, ids, {'state': 'done'})
874 for line in self.browse(cr, uid, ids, context):
875 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
879 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
880 product_obj = self.pool.get('product.product')
882 return {'value': {'product_uom': product_uos,
883 'product_uom_qty': product_uos_qty}, 'domain': {}}
885 product = product_obj.browse(cr, uid, product_id)
887 'product_uom': product.uom_id.id,
889 # FIXME must depend on uos/uom of the product and not only of the coeff.
892 'product_uom_qty': product_uos_qty / product.uos_coeff,
893 'th_weight': product_uos_qty / product.uos_coeff * product.weight
895 except ZeroDivisionError:
897 return {'value': value}
899 def copy_data(self, cr, uid, id, default=None, context={}):
902 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
903 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
905 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
906 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
907 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
909 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.'))
911 product_uom_obj = self.pool.get('product.uom')
912 partner_obj = self.pool.get('res.partner')
913 product_obj = self.pool.get('product.product')
915 lang = partner_obj.browse(cr, uid, partner_id).lang
916 context = {'lang': lang, 'partner_id': partner_id}
919 return {'value': {'th_weight': 0, 'product_packaging': False,
920 'product_uos_qty': qty}, 'domain': {'product_uom': [],
924 date_order = time.strftime('%Y-%m-%d')
927 product_obj = product_obj.browse(cr, uid, product, context=context)
928 if not packaging and product_obj.packaging:
929 packaging = product_obj.packaging[0].id
930 result['product_packaging'] = packaging
933 default_uom = product_obj.uom_id and product_obj.uom_id.id
934 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
935 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
936 # qty = qty - qty % q + q
937 if qty and (q and not (qty % q) == 0):
941 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)
942 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
944 'title': _('Picking Information !'),
947 result['product_uom_qty'] = qty
950 uom2 = product_uom_obj.browse(cr, uid, uom)
951 if product_obj.uom_id.category_id.id != uom2.category_id.id:
955 if product_obj.uos_id:
956 uos2 = product_uom_obj.browse(cr, uid, uos)
957 if product_obj.uos_id.category_id.id != uos2.category_id.id:
961 result.update({'type': product_obj.procure_method})
962 if product_obj.description_sale:
963 result['notes'] = product_obj.description_sale
964 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
965 if update_tax: #The quantity only have changed
966 result['delay'] = (product_obj.sale_delay or 0.0)
967 partner = partner_obj.browse(cr, uid, partner_id)
968 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
970 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id])[0][1]
972 if (not uom) and (not uos):
973 result['product_uom'] = product_obj.uom_id.id
974 if product_obj.uos_id:
975 result['product_uos'] = product_obj.uos_id.id
976 result['product_uos_qty'] = qty * product_obj.uos_coeff
977 uos_category_id = product_obj.uos_id.category_id.id
979 result['product_uos'] = False
980 result['product_uos_qty'] = qty
981 uos_category_id = False
982 result['th_weight'] = qty * product_obj.weight
983 domain = {'product_uom':
984 [('category_id', '=', product_obj.uom_id.category_id.id)],
986 [('category_id', '=', uos_category_id)]}
988 elif uos: # only happens if uom is False
989 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
990 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
991 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
992 elif uom: # whether uos is set or not
993 default_uom = product_obj.uom_id and product_obj.uom_id.id
994 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
995 if product_obj.uos_id:
996 result['product_uos'] = product_obj.uos_id.id
997 result['product_uos_qty'] = qty * product_obj.uos_coeff
999 result['product_uos'] = False
1000 result['product_uos_qty'] = qty
1001 result['th_weight'] = q * product_obj.weight # Round the quantity up
1007 'title': 'No Pricelist !',
1009 'You have to select a pricelist in the sale form !\n'
1010 'Please set one before choosing a product.'
1013 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1014 product, qty or 1.0, partner_id, {
1020 'title': 'No valid pricelist line found !',
1022 "Couldn't find a pricelist line matching this product and quantity.\n"
1023 "You have to change either the product, the quantity or the pricelist."
1026 result.update({'price_unit': price})
1027 return {'value': result, 'domain': domain, 'warning': warning}
1029 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1030 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1031 lang=False, update_tax=True, date_order=False):
1032 res = self.product_id_change(cursor, user, ids, pricelist, product,
1033 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1034 partner_id=partner_id, lang=lang, update_tax=update_tax,
1035 date_order=date_order)
1036 if 'product_uom' in res['value']:
1037 del res['value']['product_uom']
1039 res['value']['price_unit'] = 0.0
1042 def unlink(self, cr, uid, ids, context={}):
1043 """Allows to delete sale order lines in draft,cancel states"""
1044 for rec in self.browse(cr, uid, ids, context=context):
1045 if rec.state not in ['draft', 'cancel']:
1046 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1047 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1052 class sale_config_picking_policy(osv.osv_memory):
1053 _name = 'sale.config.picking_policy'
1055 'name': fields.char('Name', size=64),
1056 'picking_policy': fields.selection([
1057 ('direct', 'Direct Delivery'),
1058 ('one', 'All at Once')
1059 ], 'Picking Default Policy', required=True),
1060 'order_policy': fields.selection([
1061 ('manual', 'Invoice Based on Sales Orders'),
1062 ('picking', 'Invoice Based on Deliveries'),
1063 ], 'Shipping Default Policy', required=True),
1064 'step': fields.selection([
1065 ('one', 'Delivery Order Only'),
1066 ('two', 'Picking List & Delivery Order')
1067 ], 'Steps To Deliver a Sale Order', required=True,
1068 help="By default, Open ERP is able to manage complex routing and paths "\
1069 "of products in your warehouse and partner locations. This will configure "\
1070 "the most common and simple methods to deliver products to the customer "\
1071 "in one or two operations by the worker.")
1074 'picking_policy': lambda *a: 'direct',
1075 'order_policy': lambda *a: 'picking',
1076 'step': lambda *a: 'one'
1079 def set_default(self, cr, uid, ids, context=None):
1080 for o in self.browse(cr, uid, ids, context=context):
1081 ir_values_obj = self.pool.get('ir.values')
1082 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1083 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1086 md = self.pool.get('ir.model.data')
1087 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1088 group_id = md.browse(cr, uid, group_id, context).res_id
1089 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1090 menu_id = md.browse(cr, uid, menu_id, context).res_id
1091 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1093 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1094 location_id = md.browse(cr, uid, location_id, context).res_id
1095 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1098 'view_type': 'form',
1099 "view_mode": 'form',
1100 'res_model': 'ir.actions.configuration.wizard',
1101 'type': 'ir.actions.act_window',
1105 def action_cancel(self, cr, uid, ids, context=None):
1107 'view_type': 'form',
1108 "view_mode": 'form',
1109 'res_model': 'ir.actions.configuration.wizard',
1110 'type': 'ir.actions.act_window',
1114 sale_config_picking_policy()