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 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),
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 '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?"""),
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 the Packing'),
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 packing lists have been finished.
221 - The 'Invoice from the packing' 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 Packing', 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, 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, 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, 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')
259 'picking_policy': lambda *a: 'direct',
260 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
261 'order_policy': lambda *a: 'manual',
262 'state': lambda *a: 'draft',
263 'user_id': lambda obj, cr, uid, context: uid,
264 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
265 'invoice_quantity': lambda *a: 'order',
266 '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'],
267 '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'],
268 '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'],
269 '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,
274 def unlink(self, cr, uid, ids, context=None):
275 sale_orders = self.read(cr, uid, ids, ['state'])
277 for s in sale_orders:
278 if s['state'] in ['draft', 'cancel']:
279 unlink_ids.append(s['id'])
281 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
282 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
284 def onchange_shop_id(self, cr, uid, ids, shop_id):
287 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
288 v['project_id'] = shop.project_id.id
289 # Que faire si le client a une pricelist a lui ?
290 if shop.pricelist_id.id:
291 v['pricelist_id'] = shop.pricelist_id.id
292 #v['payment_default_id']=shop.payment_default_id.id
295 def action_cancel_draft(self, cr, uid, ids, *args):
298 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
299 line_ids = map(lambda x: x[0], cr.fetchall())
300 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
301 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
302 wf_service = netsvc.LocalService("workflow")
304 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
307 def onchange_partner_id(self, cr, uid, ids, part):
309 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
310 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
311 part = self.pool.get('res.partner').browse(cr, uid, part)
312 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
313 payment_term = part.property_payment_term and part.property_payment_term.id or False
314 fiscal_position = part.property_account_position and part.property_account_position.id or False
315 val = {'partner_invoice_id': addr['invoice'], 'partner_order_id': addr['contact'], 'partner_shipping_id': addr['delivery'], 'payment_term': payment_term, 'fiscal_position': fiscal_position}
317 val['pricelist_id'] = pricelist
318 return {'value': val}
320 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
324 if policy == 'prepaid':
326 elif policy == 'picking':
327 inv_qty = 'procurement'
328 return {'value': {'invoice_quantity': inv_qty}}
330 def write(self, cr, uid, ids, vals, context=None):
331 if 'order_policy' in vals:
332 if vals['order_policy'] == 'prepaid':
333 vals.update({'invoice_quantity': 'order'})
334 elif vals['order_policy'] == 'picking':
335 vals.update({'invoice_quantity': 'procurement'})
336 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
338 def create(self, cr, uid, vals, context={}):
339 if 'order_policy' in vals:
340 if vals['order_policy'] == 'prepaid':
341 vals.update({'invoice_quantity': 'order'})
342 if vals['order_policy'] == 'picking':
343 vals.update({'invoice_quantity': 'procurement'})
344 return super(sale_order, self).create(cr, uid, vals, context=context)
346 def button_dummy(self, cr, uid, ids, context={}):
349 #FIXME: the method should return the list of invoices created (invoice_ids)
350 # and not the id of the last invoice created (res). The problem is that we
351 # cannot change it directly since the method is called by the sale order
352 # workflow and I suppose it expects a single id...
353 def _inv_get(self, cr, uid, order, context={}):
356 def _make_invoice(self, cr, uid, order, lines, context={}):
357 a = order.partner_id.property_account_receivable.id
358 if order.payment_term:
359 pay_term = order.payment_term.id
362 for preinv in order.invoice_ids:
363 if preinv.state not in ('cancel',):
364 for preline in preinv.invoice_line:
365 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
366 lines.append(inv_line_id)
368 'name': order.client_order_ref or order.name,
369 'origin': order.name,
370 'type': 'out_invoice',
371 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
373 'partner_id': order.partner_id.id,
374 'address_invoice_id': order.partner_invoice_id.id,
375 'address_contact_id': order.partner_order_id.id,
376 'invoice_line': [(6, 0, lines)],
377 'currency_id': order.pricelist_id.currency_id.id,
378 'comment': order.note,
379 'payment_term': pay_term,
380 'fiscal_position': order.partner_id.property_account_position.id,
381 'date_invoice' : context.get('date_invoice',False)
383 inv_obj = self.pool.get('account.invoice')
384 inv.update(self._inv_get(cr, uid, order))
385 inv_id = inv_obj.create(cr, uid, inv)
386 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
387 if data.get('value', False):
388 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
389 inv_obj.button_compute(cr, uid, [inv_id])
392 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
398 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
399 # last day of the last month as invoice date
401 context['date_inv'] = date_inv
402 for o in self.browse(cr,uid,ids):
404 for line in o.order_line:
405 if (line.state in states) and not line.invoiced:
406 lines.append(line.id)
407 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
409 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
412 for o in self.browse(cr, uid, ids):
413 for i in o.invoice_ids:
414 if i.state == 'draft':
416 picking_obj = self.pool.get('stock.picking')
417 for val in invoices.values():
419 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
421 self.write(cr, uid, [o.id], {'state' : 'progress'})
422 if o.order_policy=='picking':
423 picking_obj.write(cr,uid,map(lambda x:x.id,o.picking_ids),{'invoice_state':'invoiced'})
424 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
426 for order, il in val:
427 res = self._make_invoice(cr, uid, order, il, context=context)
428 invoice_ids.append(res)
429 self.write(cr, uid, [order.id], {'state': 'progress'})
430 if order.order_policy == 'picking':
431 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
432 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
435 def action_invoice_cancel(self, cr, uid, ids, context={}):
436 for sale in self.browse(cr, uid, ids):
437 for line in sale.order_line:
439 for iline in line.invoice_lines:
440 if iline.invoice_id and iline.invoice_id.state == 'cancel':
444 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
445 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
448 def action_cancel(self, cr, uid, ids, context={}):
450 sale_order_line_obj = self.pool.get('sale.order.line')
451 for sale in self.browse(cr, uid, ids):
452 for pick in sale.picking_ids:
453 if pick.state not in ('draft', 'cancel'):
454 raise osv.except_osv(
455 _('Could not cancel sale order !'),
456 _('You must first cancel all packing attached to this sale order.'))
457 for r in self.read(cr, uid, ids, ['picking_ids']):
458 for pick in r['picking_ids']:
459 wf_service = netsvc.LocalService("workflow")
460 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
461 for inv in sale.invoice_ids:
462 if inv.state not in ('draft', 'cancel'):
463 raise osv.except_osv(
464 _('Could not cancel this sale order !'),
465 _('You must first cancel all invoices attached to this sale order.'))
466 for r in self.read(cr, uid, ids, ['invoice_ids']):
467 for inv in r['invoice_ids']:
468 wf_service = netsvc.LocalService("workflow")
469 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
470 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
472 self.write(cr, uid, ids, {'state': 'cancel'})
475 def action_wait(self, cr, uid, ids, *args):
476 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
477 event_obj = self.pool.get('res.partner.event')
478 for o in self.browse(cr, uid, ids):
480 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
481 'partner_id': o.partner_id.id,\
482 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
483 'user_id': (o.user_id and o.user_id.id) or uid,\
484 'partner_type': 'customer', 'probability': 1.0,\
485 'planned_revenue': o.amount_untaxed})
486 if (o.order_policy == 'manual'):
487 self.write(cr, uid, [o.id], {'state': 'manual'})
489 self.write(cr, uid, [o.id], {'state': 'progress'})
490 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
492 def procurement_lines_get(self, cr, uid, ids, *args):
494 for order in self.browse(cr, uid, ids, context={}):
495 for line in order.order_line:
496 if line.procurement_id:
497 res.append(line.procurement_id.id)
500 # if mode == 'finished':
501 # returns True if all lines are done, False otherwise
502 # if mode == 'canceled':
503 # returns True if there is at least one canceled line, False otherwise
504 def test_state(self, cr, uid, ids, mode, *args):
505 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
510 write_cancel_ids = []
511 stock_move_obj = self.pool.get('stock.move')
512 for order in self.browse(cr, uid, ids, context={}):
514 #check for pending deliveries
515 pending_deliveries = False
516 # check => if order_lines do not exist,don't proceed for any mode.
517 if not order.order_line:
519 for line in order.order_line:
520 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
521 for move in stock_move_obj.browse( cr, uid, move_ids ):
522 #if one of the related order lines is in state draft, auto or confirmed
523 #this order line is not yet delivered
524 if move.state in ('draft', 'auto', 'confirmed'):
525 pending_deliveries = True
526 # Reason => if there are no move lines,the following condition will always set to be true,and will set SO to 'DONE'.
527 # Added move_ids check to SOLVE.
528 if move_ids and ((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)
542 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
544 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
546 if mode == 'finished':
548 elif mode == 'canceled':
554 def action_ship_create(self, cr, uid, ids, *args):
556 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
557 for order in self.browse(cr, uid, ids, context={}):
558 output_id = order.shop_id.warehouse_id.lot_output_id.id
560 for line in order.order_line:
562 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
563 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
564 if line.state == 'done':
566 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
567 location_id = order.shop_id.warehouse_id.lot_stock_id.id
569 loc_dest_id = order.partner_id.property_stock_customer.id
570 picking_id = self.pool.get('stock.picking').create(cr, uid, {
571 'origin': order.name,
574 'move_type': order.picking_policy,
576 'address_id': order.partner_shipping_id.id,
578 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
582 move_id = self.pool.get('stock.move').create(cr, uid, {
583 'name': line.name[:64],
584 'picking_id': picking_id,
585 'product_id': line.product_id.id,
586 'date_planned': date_planned,
587 'product_qty': line.product_uom_qty,
588 'product_uom': line.product_uom.id,
589 'product_uos_qty': line.product_uos_qty,
590 'product_uos': (line.product_uos and line.product_uos.id)\
591 or line.product_uom.id,
592 'product_packaging': line.product_packaging.id,
593 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
594 'location_id': location_id,
595 'location_dest_id': output_id,
596 'sale_line_id': line.id,
597 'tracking_id': False,
602 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
604 'origin': order.name,
605 'date_planned': date_planned,
606 'product_id': line.product_id.id,
607 'product_qty': line.product_uom_qty,
608 'product_uom': line.product_uom.id,
609 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
610 or line.product_uom_qty,
611 'product_uos': (line.product_uos and line.product_uos.id)\
612 or line.product_uom.id,
613 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
614 'procure_method': line.type,
616 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
618 wf_service = netsvc.LocalService("workflow")
619 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
620 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
621 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
622 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
624 'origin': order.name,
625 'date_planned': date_planned,
626 'product_id': line.product_id.id,
627 'product_qty': line.product_uom_qty,
628 'product_uom': line.product_uom.id,
629 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
630 'procure_method': line.type,
631 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
633 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
634 wf_service = netsvc.LocalService("workflow")
635 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
638 # No procurement because no product in the sale.order.line.
644 wf_service = netsvc.LocalService("workflow")
645 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
647 if order.state == 'shipping_except':
648 val['state'] = 'progress'
650 if (order.order_policy == 'manual'):
651 for line in order.order_line:
652 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
653 val['state'] = 'manual'
655 self.write(cr, uid, [order.id], val)
659 def action_ship_end(self, cr, uid, ids, context={}):
660 for order in self.browse(cr, uid, ids):
661 val = {'shipped': True}
662 if order.state == 'shipping_except':
663 val['state'] = 'progress'
664 if (order.order_policy == 'manual'):
665 for line in order.order_line:
666 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
667 val['state'] = 'manual'
669 for line in order.order_line:
671 if line.state == 'exception':
672 towrite.append(line.id)
674 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
675 self.write(cr, uid, [order.id], val)
678 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
679 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
681 part = inv['partner_id'] and inv['partner_id'][0]
682 pr = inv['amount_untaxed'] or 0.0
683 partnertype = 'customer'
686 'name': 'Order: '+name,
688 'description': 'Order '+str(inv['id']),
691 'date': time.strftime('%Y-%m-%d'),
694 'partner_type': partnertype,
696 'planned_revenue': pr,
700 self.pool.get('res.partner.event').create(cr, uid, event)
702 def has_stockable_products(self, cr, uid, ids, *args):
703 for order in self.browse(cr, uid, ids):
704 for order_line in order.order_line:
705 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
710 # TODO add a field price_unit_uos
711 # - update it on change product and unit price
712 # - use it in report if there is a uos
713 class sale_order_line(osv.osv):
714 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
716 for line in self.browse(cr, uid, ids):
717 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
720 def _amount_line(self, cr, uid, ids, field_name, arg, context):
722 cur_obj = self.pool.get('res.currency')
723 for line in self.browse(cr, uid, ids):
724 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
725 cur = line.order_id.pricelist_id.currency_id
726 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
729 def _number_packages(self, cr, uid, ids, field_name, arg, context):
731 for line in self.browse(cr, uid, ids):
733 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
738 _name = 'sale.order.line'
739 _description = 'Sale Order line'
741 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
742 'name': fields.char('Description', size=256, required=True, select=True),
743 'sequence': fields.integer('Sequence'),
744 '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"),
745 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
746 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
747 'invoiced': fields.boolean('Invoiced', readonly=True),
748 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
749 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
750 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
751 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
752 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
753 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True),
754 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
755 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
756 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
757 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
758 'product_uos_qty': fields.float('Quantity (UoS)'),
759 'product_uos': fields.many2one('product.uom', 'Product UoS'),
760 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
761 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
762 'discount': fields.float('Discount (%)', digits=(16, 2)),
763 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
764 'notes': fields.text('Notes'),
765 'th_weight': fields.float('Weight'),
766 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
767 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
768 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
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)
828 raise osv.except_osv(_('Error !'),
829 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
830 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
832 'origin': line.order_id.name,
836 'discount': line.discount,
838 'product_id': line.product_id.id or False,
839 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
841 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
843 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
844 self.write(cr, uid, [line.id], {'invoiced': True})
846 sales[line.order_id.id] = True
847 create_ids.append(inv_id)
849 # Trigger workflow events
850 wf_service = netsvc.LocalService("workflow")
851 for sid in sales.keys():
852 wf_service.trg_write(uid, 'sale.order', sid, cr)
855 def button_cancel(self, cr, uid, ids, context={}):
856 for line in self.browse(cr, uid, ids, context=context):
858 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
859 return self.write(cr, uid, ids, {'state': 'cancel'})
861 def button_confirm(self, cr, uid, ids, context={}):
862 return self.write(cr, uid, ids, {'state': 'confirmed'})
864 def button_done(self, cr, uid, ids, context={}):
865 wf_service = netsvc.LocalService("workflow")
866 res = self.write(cr, uid, ids, {'state': 'done'})
867 for line in self.browse(cr, uid, ids, context):
868 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
872 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
873 product_obj = self.pool.get('product.product')
875 return {'value': {'product_uom': product_uos,
876 'product_uom_qty': product_uos_qty}, 'domain': {}}
878 product = product_obj.browse(cr, uid, product_id)
880 'product_uom': product.uom_id.id,
882 # FIXME must depend on uos/uom of the product and not only of the coeff.
885 'product_uom_qty': product_uos_qty / product.uos_coeff,
886 'th_weight': product_uos_qty / product.uos_coeff * product.weight
888 except ZeroDivisionError:
890 return {'value': value}
892 def copy_data(self, cr, uid, id, default=None, context={}):
895 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
896 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
898 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
899 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
900 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
902 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.'))
904 product_uom_obj = self.pool.get('product.uom')
905 partner_obj = self.pool.get('res.partner')
906 product_obj = self.pool.get('product.product')
908 lang = partner_obj.browse(cr, uid, partner_id).lang
909 context = {'lang': lang, 'partner_id': partner_id}
912 return {'value': {'th_weight': 0, 'product_packaging': False,
913 'product_uos_qty': qty}, 'domain': {'product_uom': [],
917 date_order = time.strftime('%Y-%m-%d')
920 product_obj = product_obj.browse(cr, uid, product, context=context)
921 if not packaging and product_obj.packaging:
922 packaging = product_obj.packaging[0].id
923 result['product_packaging'] = packaging
926 default_uom = product_obj.uom_id and product_obj.uom_id.id
927 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
928 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
929 # qty = qty - qty % q + q
930 if qty and (q and not (qty % q) == 0):
934 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)
935 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
937 'title': _('Packing Information !'),
940 result['product_uom_qty'] = qty
943 uom2 = product_uom_obj.browse(cr, uid, uom)
944 if product_obj.uom_id.category_id.id != uom2.category_id.id:
948 if product_obj.uos_id:
949 uos2 = product_uom_obj.browse(cr, uid, uos)
950 if product_obj.uos_id.category_id.id != uos2.category_id.id:
954 result.update({'type': product_obj.procure_method})
955 if product_obj.description_sale:
956 result['notes'] = product_obj.description_sale
957 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
958 if update_tax: #The quantity only have changed
959 result['delay'] = (product_obj.sale_delay or 0.0)
960 partner = partner_obj.browse(cr, uid, partner_id)
961 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
963 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id])[0][1]
965 if (not uom) and (not uos):
966 result['product_uom'] = product_obj.uom_id.id
967 if product_obj.uos_id:
968 result['product_uos'] = product_obj.uos_id.id
969 result['product_uos_qty'] = qty * product_obj.uos_coeff
970 uos_category_id = product_obj.uos_id.category_id.id
972 result['product_uos'] = False
973 result['product_uos_qty'] = qty
974 uos_category_id = False
975 result['th_weight'] = qty * product_obj.weight
976 domain = {'product_uom':
977 [('category_id', '=', product_obj.uom_id.category_id.id)],
979 [('category_id', '=', uos_category_id)]}
981 elif uos: # only happens if uom is False
982 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
983 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
984 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
985 elif uom: # whether uos is set or not
986 default_uom = product_obj.uom_id and product_obj.uom_id.id
987 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
988 if product_obj.uos_id:
989 result['product_uos'] = product_obj.uos_id.id
990 result['product_uos_qty'] = qty * product_obj.uos_coeff
992 result['product_uos'] = False
993 result['product_uos_qty'] = qty
994 result['th_weight'] = q * product_obj.weight # Round the quantity up
1000 'title': 'No Pricelist !',
1002 'You have to select a pricelist in the sale form !\n'
1003 'Please set one before choosing a product.'
1006 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1007 product, qty or 1.0, partner_id, {
1013 'title': 'No valid pricelist line found !',
1015 "Couldn't find a pricelist line matching this product and quantity.\n"
1016 "You have to change either the product, the quantity or the pricelist."
1019 result.update({'price_unit': price})
1020 return {'value': result, 'domain': domain, 'warning': warning}
1022 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1023 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1024 lang=False, update_tax=True, date_order=False):
1025 res = self.product_id_change(cursor, user, ids, pricelist, product,
1026 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1027 partner_id=partner_id, lang=lang, update_tax=update_tax,
1028 date_order=date_order)
1029 if 'product_uom' in res['value']:
1030 del res['value']['product_uom']
1032 res['value']['price_unit'] = 0.0
1035 def unlink(self, cr, uid, ids, context={}):
1036 """Allows to delete sale order lines in draft,cancel states"""
1037 for rec in self.browse(cr, uid, ids, context=context):
1038 if rec.state not in ['draft', 'cancel']:
1039 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1040 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1045 class sale_config_picking_policy(osv.osv_memory):
1046 _name = 'sale.config.picking_policy'
1048 'name': fields.char('Name', size=64),
1049 'picking_policy': fields.selection([
1050 ('direct', 'Direct Delivery'),
1051 ('one', 'All at Once')
1052 ], 'Packing Default Policy', required=True),
1053 'order_policy': fields.selection([
1054 ('manual', 'Invoice Based on Sales Orders'),
1055 ('picking', 'Invoice Based on Deliveries'),
1056 ], 'Shipping Default Policy', required=True),
1057 'step': fields.selection([
1058 ('one', 'Delivery Order Only'),
1059 ('two', 'Packing List & Delivery Order')
1060 ], 'Steps To Deliver a Sale Order', required=True,
1061 help="By default, Open ERP is able to manage complex routing and paths "\
1062 "of products in your warehouse and partner locations. This will configure "\
1063 "the most common and simple methods to deliver products to the customer "\
1064 "in one or two operations by the worker.")
1067 'picking_policy': lambda *a: 'direct',
1068 'order_policy': lambda *a: 'picking',
1069 'step': lambda *a: 'one'
1072 def set_default(self, cr, uid, ids, context=None):
1073 for o in self.browse(cr, uid, ids, context=context):
1074 ir_values_obj = self.pool.get('ir.values')
1075 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1076 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1079 md = self.pool.get('ir.model.data')
1080 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1081 group_id = md.browse(cr, uid, group_id, context).res_id
1082 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1083 menu_id = md.browse(cr, uid, menu_id, context).res_id
1084 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1086 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1087 location_id = md.browse(cr, uid, location_id, context).res_id
1088 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1091 'view_type': 'form',
1092 "view_mode": 'form',
1093 'res_model': 'ir.actions.configuration.wizard',
1094 'type': 'ir.actions.act_window',
1098 def action_cancel(self, cr, uid, ids, context=None):
1100 'view_type': 'form',
1101 "view_mode": 'form',
1102 'res_model': 'ir.actions.configuration.wizard',
1103 'type': 'ir.actions.act_window',
1107 sale_config_picking_policy()