1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 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 datetime import datetime
26 from dateutil.relativedelta import relativedelta
27 from tools import config
28 from tools.translate import _
30 import decimal_precision as dp
33 class sale_shop(osv.osv):
35 _description = "Sale Shop"
37 'name': fields.char('Shop Name', size=64, required=True),
38 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
39 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
40 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
41 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
42 'company_id': fields.many2one('res.company', 'Company'),
47 def _incoterm_get(self, cr, uid, context=None):
50 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
53 class sale_order(osv.osv):
56 _description = "Sale Order"
58 def copy(self, cr, uid, id, default=None, context=None):
68 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
70 return super(sale_order, self).copy(cr, uid, id, default, context=context)
72 def _amount_line_tax(self, cr, uid, line, context=None):
76 for c in self.pool.get('account.tax').compute_all(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)['taxes']:
80 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
84 cur_obj = self.pool.get('res.currency')
85 for order in self.browse(cr, uid, ids, context):
87 'amount_untaxed': 0.0,
92 cur = order.pricelist_id.currency_id
93 for line in order.order_line:
94 val1 += line.price_subtotal
95 val += self._amount_line_tax(cr, uid, line, context=context)
96 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
97 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
98 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
101 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
110 p.sale_id,sum(m.product_qty), mp.state as mp_state
114 stock_picking p on (p.id=m.picking_id)
116 procurement_order mp on (mp.move_id=m.id)
118 p.sale_id = ANY(%s) GROUP BY mp.state, p.sale_id''',(ids,))
119 for oid, nbr, mp_state in cr.fetchall():
120 if mp_state == 'cancel':
122 if mp_state == 'done':
123 res[oid][0] += nbr or 0.0
124 res[oid][1] += nbr or 0.0
126 res[oid][1] += nbr or 0.0
131 res[r] = 100.0 * res[r][0] / res[r][1]
132 for order in self.browse(cr, uid, ids, context=context):
134 res[order.id] = 100.0
137 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
141 for sale in self.browse(cursor, user, ids, context=context):
146 for invoice in sale.invoice_ids:
147 if invoice.state not in ('draft', 'cancel'):
148 tot += invoice.amount_untaxed
151 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
156 def _invoiced(self, cursor, user, ids, name, arg, context=None):
160 for sale in self.browse(cursor, user, ids, context=context):
162 for invoice in sale.invoice_ids:
163 if invoice.state != 'paid':
166 if not sale.invoice_ids:
170 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
180 clause += 'AND inv.state = \'paid\''
182 clause += 'AND inv.state <> \'paid\''
184 cursor.execute('SELECT rel.order_id ' \
185 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
186 'WHERE rel.invoice_id = inv.id ' + clause)
187 res = cursor.fetchall()
189 cursor.execute('SELECT sale.id ' \
190 'FROM sale_order AS sale ' \
191 'WHERE sale.id NOT IN ' \
192 '(SELECT rel.order_id ' \
193 'FROM sale_order_invoice_rel AS rel)')
194 res.extend(cursor.fetchall())
196 return [('id', '=', 0)]
197 return [('id', 'in', [x[0] for x in res])]
199 def _get_order(self, cr, uid, ids, context=None):
203 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
204 result[line.order_id.id] = True
208 'name': fields.char('Order Reference', size=64, required=True, select=True),
209 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
210 'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
211 'client_order_ref': fields.char('Customer Reference', size=64),
213 'state': fields.selection([
214 ('draft', 'Quotation'),
215 ('waiting_date', 'Waiting Schedule'),
216 ('manual', 'Manual In Progress'),
217 ('progress', 'In Progress'),
218 ('shipping_except', 'Shipping Exception'),
219 ('invoice_except', 'Invoice Exception'),
221 ('cancel', 'Cancelled')
222 ], '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 'Ordered Date'.", select=True),
223 'date_order': fields.date('Ordered Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224 'create_date': fields.date('Creation Date', readonly=True),
225 'date_confirm': fields.date('Confirmation Date', readonly=True),
226 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
227 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
228 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
229 '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 who requested the order or quotation."),
230 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sale order"),
232 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
233 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
234 '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?"""),
235 'order_policy': fields.selection([
236 ('prepaid', 'Payment Before Delivery'),
237 ('manual', 'Shipping & Manual Invoice'),
238 ('postpaid', 'Invoice on Order After Delivery'),
239 ('picking', 'Invoice from Picking'),
240 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
241 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
242 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
243 - The 'Shipping & Manual Invoice' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
244 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
245 - The 'Invoice from the picking' choice is used to create an invoice during the picking process."""),
246 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist version for current sale order"),
247 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
249 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
250 '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)."),
251 '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"),
252 'shipped': fields.boolean('Picked', readonly=True),
253 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
254 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
255 'invoiced': fields.function(_invoiced, method=True, string='Paid',
256 fnct_search=_invoiced_search, type='boolean'),
257 'note': fields.text('Notes'),
259 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
261 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
262 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
265 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
267 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
268 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
271 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Total',
273 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
274 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
278 '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),
279 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
280 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
281 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
284 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', context=c),
285 'picking_policy': lambda *a: 'direct',
286 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
287 'order_policy': lambda *a: 'manual',
288 'state': lambda *a: 'draft',
289 'user_id': lambda obj, cr, uid, context: uid,
290 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
291 'invoice_quantity': lambda *a: 'order',
292 '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'],
293 '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'],
294 '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'],
295 # '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,
300 def unlink(self, cr, uid, ids, context=None):
303 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
305 for s in sale_orders:
306 if s['state'] in ['draft', 'cancel']:
307 unlink_ids.append(s['id'])
309 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
310 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
312 def onchange_shop_id(self, cr, uid, ids, shop_id):
315 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
316 v['project_id'] = shop.project_id.id
317 # Que faire si le client a une pricelist a lui ?
318 if shop.pricelist_id.id:
319 v['pricelist_id'] = shop.pricelist_id.id
320 #v['payment_default_id']=shop.payment_default_id.id
323 def action_cancel_draft(self, cr, uid, ids, *args):
326 cr.execute('select id from sale_order_line where order_id = ANY(%s) and state=%s',(ids,'cancel'))
327 line_ids = map(lambda x: x[0], cr.fetchall())
328 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
329 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
330 wf_service = netsvc.LocalService("workflow")
332 # Deleting the existing instance of workflow for SO
333 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
334 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
337 def onchange_partner_id(self, cr, uid, ids, part):
339 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
341 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
342 part = self.pool.get('res.partner').browse(cr, uid, part)
343 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
344 payment_term = part.property_payment_term and part.property_payment_term.id or False
345 fiscal_position = part.property_account_position and part.property_account_position.id or False
346 dedicated_salesman = part.user_id and part.user_id.id or uid
349 'partner_invoice_id': addr['invoice'],
350 'partner_order_id': addr['contact'],
351 'partner_shipping_id': addr['delivery'],
352 'payment_term': payment_term,
353 'fiscal_position': fiscal_position,
354 'user_id': dedicated_salesman,
358 val['pricelist_id'] = pricelist
360 return {'value': val}
362 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
368 if policy == 'prepaid':
370 elif policy == 'picking':
371 inv_qty = 'procurement'
372 return {'value': {'invoice_quantity': inv_qty}}
374 def write(self, cr, uid, ids, vals, context=None):
377 if 'order_policy' in vals:
378 if vals['order_policy'] == 'prepaid':
379 vals.update({'invoice_quantity': 'order'})
380 elif vals['order_policy'] == 'picking':
381 vals.update({'invoice_quantity': 'procurement'})
382 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
384 def create(self, cr, uid, vals, context=None):
387 if 'order_policy' in vals:
388 if vals['order_policy'] == 'prepaid':
389 vals.update({'invoice_quantity': 'order'})
390 if vals['order_policy'] == 'picking':
391 vals.update({'invoice_quantity': 'procurement'})
392 return super(sale_order, self).create(cr, uid, vals, context=context)
394 def button_dummy(self, cr, uid, ids, context=None):
399 #FIXME: the method should return the list of invoices created (invoice_ids)
400 # and not the id of the last invoice created (res). The problem is that we
401 # cannot change it directly since the method is called by the sale order
402 # workflow and I suppose it expects a single id...
403 def _inv_get(self, cr, uid, order, context=None):
408 def _make_invoice(self, cr, uid, order, lines, context=None):
411 a = order.partner_id.property_account_receivable.id
412 if order.payment_term:
413 pay_term = order.payment_term.id
416 for preinv in order.invoice_ids:
417 if preinv.state not in ('cancel',):
418 for preline in preinv.invoice_line:
419 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
420 lines.append(inv_line_id)
421 journal_obj = self.pool.get('account.journal')
422 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
424 raise osv.except_osv(_('Error !'),
425 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
428 'name': order.client_order_ref or order.name,
429 'origin': order.name,
430 'type': 'out_invoice',
431 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
433 'partner_id': order.partner_id.id,
434 'journal_id': journal_ids[0],
435 'address_invoice_id': order.partner_invoice_id.id,
436 'address_contact_id': order.partner_order_id.id,
437 'invoice_line': [(6, 0, lines)],
438 'currency_id': order.pricelist_id.currency_id.id,
439 'comment': order.note,
440 'payment_term': pay_term,
441 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
442 'date_invoice' : context.get('date_invoice',False),
443 'company_id' : order.company_id.id,
444 'user_id':order.user_id and order.user_id.id or False
446 inv_obj = self.pool.get('account.invoice')
447 inv.update(self._inv_get(cr, uid, order))
448 inv_id = inv_obj.create(cr, uid, inv, context=context)
449 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
450 if data.get('value', False):
451 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
452 inv_obj.button_compute(cr, uid, [inv_id])
455 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
461 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
462 # last day of the last month as invoice date
464 context['date_inv'] = date_inv
465 for o in self.browse(cr, uid, ids):
467 for line in o.order_line:
468 if (line.state in states) and not line.invoiced:
469 lines.append(line.id)
470 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
472 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
475 for o in self.browse(cr, uid, ids):
476 for i in o.invoice_ids:
477 if i.state == 'draft':
479 picking_obj = self.pool.get('stock.picking')
480 for val in invoices.values():
482 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
484 self.write(cr, uid, [o.id], {'state': 'progress'})
485 if o.order_policy == 'picking':
486 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
487 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
489 for order, il in val:
490 res = self._make_invoice(cr, uid, order, il, context=context)
491 invoice_ids.append(res)
492 self.write(cr, uid, [order.id], {'state': 'progress'})
493 if order.order_policy == 'picking':
494 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
495 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
498 def action_invoice_cancel(self, cr, uid, ids, context=None):
501 for sale in self.browse(cr, uid, ids, context=context):
502 for line in sale.order_line:
504 for iline in line.invoice_lines:
505 if iline.invoice_id and iline.invoice_id.state == 'cancel':
509 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
510 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
513 def action_invoice_end(self, cr, uid, ids, context=None):
517 for order in self.browse(cr, uid, ids, context=context):
518 for line in order.order_line:
519 if line.state == 'exception':
520 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'state': 'confirmed'}, context=context)
522 if order.state == 'invoice_except':
523 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
527 def action_cancel(self, cr, uid, ids, context=None):
531 sale_order_line_obj = self.pool.get('sale.order.line')
532 for sale in self.browse(cr, uid, ids, context=context):
533 for pick in sale.picking_ids:
534 if pick.state not in ('draft', 'cancel'):
535 raise osv.except_osv(
536 _('Could not cancel sale order !'),
537 _('You must first cancel all picking attached to this sale order.'))
538 for r in self.read(cr, uid, ids, ['picking_ids']):
539 for pick in r['picking_ids']:
540 wf_service = netsvc.LocalService("workflow")
541 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
542 for inv in sale.invoice_ids:
543 if inv.state not in ('draft', 'cancel'):
544 raise osv.except_osv(
545 _('Could not cancel this sale order !'),
546 _('You must first cancel all invoices attached to this sale order.'))
547 for r in self.read(cr, uid, ids, ['invoice_ids']):
548 for inv in r['invoice_ids']:
549 wf_service = netsvc.LocalService("workflow")
550 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
551 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
553 self.write(cr, uid, ids, {'state': 'cancel'})
556 def action_wait(self, cr, uid, ids, *args):
557 for (id,name) in self.name_get(cr, uid, ids):
558 message = _('Quotation ') + " '" + name + "' "+ _("converted to sale order.")
559 self.log(cr, uid, id, message)
560 for o in self.browse(cr, uid, ids):
561 if (o.order_policy == 'manual'):
562 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
564 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
565 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
567 def procurement_lines_get(self, cr, uid, ids, *args):
569 for order in self.browse(cr, uid, ids, context={}):
570 for line in order.order_line:
571 if line.procurement_id:
572 res.append(line.procurement_id.id)
575 # if mode == 'finished':
576 # returns True if all lines are done, False otherwise
577 # if mode == 'canceled':
578 # returns True if there is at least one canceled line, False otherwise
579 def test_state(self, cr, uid, ids, mode, *args):
580 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
585 write_cancel_ids = []
586 for order in self.browse(cr, uid, ids, context={}):
587 for line in order.order_line:
588 if (not line.procurement_id) or (line.procurement_id.state=='done'):
589 if line.state != 'done':
590 write_done_ids.append(line.id)
593 if line.procurement_id:
594 if (line.procurement_id.state == 'cancel'):
596 if line.state != 'exception':
597 write_cancel_ids.append(line.id)
601 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
603 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
605 if mode == 'finished':
607 elif mode == 'canceled':
613 def action_ship_create(self, cr, uid, ids, *args):
615 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
616 for order in self.browse(cr, uid, ids, context={}):
617 output_id = order.shop_id.warehouse_id.lot_output_id.id
619 for line in order.order_line:
621 date_planned = datetime.now() + relativedelta(days=line.delay or 0.0)
622 date_planned = (date_planned - relativedelta(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
623 if line.state == 'done':
625 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
626 location_id = order.shop_id.warehouse_id.lot_stock_id.id
628 loc_dest_id = order.partner_id.property_stock_customer.id
629 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
630 picking_id = self.pool.get('stock.picking').create(cr, uid, {
632 'origin': order.name,
635 'move_type': order.picking_policy,
637 'address_id': order.partner_shipping_id.id,
639 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
640 'company_id': order.company_id.id,
643 move_id = self.pool.get('stock.move').create(cr, uid, {
644 'name': line.name[:64],
645 'picking_id': picking_id,
646 'product_id': line.product_id.id,
647 'date_planned': date_planned,
648 'date_expected': date_planned,
649 'product_qty': line.product_uom_qty,
650 'product_uom': line.product_uom.id,
651 'product_uos_qty': line.product_uos_qty,
652 'product_uos': (line.product_uos and line.product_uos.id)\
653 or line.product_uom.id,
654 'product_packaging': line.product_packaging.id,
655 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
656 'location_id': location_id,
657 'location_dest_id': output_id,
658 'sale_line_id': line.id,
659 'tracking_id': False,
663 'company_id': order.company_id.id,
665 proc_id = self.pool.get('procurement.order').create(cr, uid, {
667 'origin': order.name,
668 'date_planned': date_planned,
669 'product_id': line.product_id.id,
670 'product_qty': line.product_uom_qty,
671 'product_uom': line.product_uom.id,
672 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
673 or line.product_uom_qty,
674 'product_uos': (line.product_uos and line.product_uos.id)\
675 or line.product_uom.id,
676 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
677 'procure_method': line.type,
679 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
680 'company_id': order.company_id.id,
682 wf_service = netsvc.LocalService("workflow")
683 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
684 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
685 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
686 proc_id = self.pool.get('procurement.order').create(cr, uid, {
688 'origin': order.name,
689 'date_planned': date_planned,
690 'product_id': line.product_id.id,
691 'product_qty': line.product_uom_qty,
692 'product_uom': line.product_uom.id,
693 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
694 'procure_method': line.type,
695 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
696 'company_id': order.company_id.id,
698 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
699 wf_service = netsvc.LocalService("workflow")
700 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
703 # No procurement because no product in the sale.order.line.
709 wf_service = netsvc.LocalService("workflow")
710 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
712 if order.state == 'shipping_except':
713 val['state'] = 'progress'
715 if (order.order_policy == 'manual'):
716 for line in order.order_line:
717 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
718 val['state'] = 'manual'
720 self.write(cr, uid, [order.id], val)
724 def action_ship_end(self, cr, uid, ids, context=None):
727 for order in self.browse(cr, uid, ids, context=context):
728 val = {'shipped': True}
729 if order.state == 'shipping_except':
730 val['state'] = 'progress'
731 if (order.order_policy == 'manual'):
732 for line in order.order_line:
733 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
734 val['state'] = 'manual'
736 for line in order.order_line:
738 if line.state == 'exception':
739 towrite.append(line.id)
741 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
742 self.write(cr, uid, [order.id], val)
745 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
746 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
748 part = inv['partner_id'] and inv['partner_id'][0]
749 pr = inv['amount_untaxed'] or 0.0
750 partnertype = 'customer'
753 'name': 'Order: '+name,
755 'description': 'Order '+str(inv['id']),
758 'date': time.strftime('%Y-%m-%d'),
761 'partner_type': partnertype,
763 'planned_revenue': pr,
767 self.pool.get('res.partner.event').create(cr, uid, event)
769 def has_stockable_products(self, cr, uid, ids, *args):
770 for order in self.browse(cr, uid, ids):
771 for order_line in order.order_line:
772 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
777 # TODO add a field price_unit_uos
778 # - update it on change product and unit price
779 # - use it in report if there is a uos
780 class sale_order_line(osv.osv):
781 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
783 context = context or {}
784 tax_obj = self.pool.get('account.tax')
785 cur_obj = self.pool.get('res.currency')
786 for line in self.browse(cr, uid, ids, context=context):
787 price = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
788 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty)
789 cur = line.order_id.pricelist_id.currency_id
790 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
793 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
797 for line in self.browse(cr, uid, ids, context=context):
799 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
804 _name = 'sale.order.line'
805 _description = 'Sale Order Line'
807 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
808 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
809 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
810 '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", readonly=True, states={'draft':[('readonly',False)]}),
811 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
812 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
813 'invoiced': fields.boolean('Invoiced', readonly=True),
814 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
815 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft':[('readonly',False)]}),
816 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
817 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
818 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
819 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
820 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
821 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
822 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
823 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
824 'product_uos': fields.many2one('product.uom', 'Product UoS'),
825 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
826 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
827 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
828 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
829 'notes': fields.text('Notes'),
830 'th_weight': fields.float('Weight'),
831 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
832 help=' * The \'Draft\' state is set automatically when sale order in draft state. \
833 \n* The \'Confirmed\' state is set automatically when sale order in confirm state. \
834 \n* The \'Exception\' state is set automatically when sale order is set as exception. \
835 \n* The \'Done\' state is set automatically when sale order is set as done. \
836 \n* The \'Cancelled\' state is set automatically when user cancel sale order.'),
837 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
838 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
839 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
841 _order = 'sequence, id'
843 'discount': lambda *a: 0.0,
844 'delay': lambda *a: 0.0,
845 'product_uom_qty': lambda *a: 1,
846 'product_uos_qty': lambda *a: 1,
847 'sequence': lambda *a: 10,
848 'invoiced': lambda *a: 0,
849 'state': lambda *a: 'draft',
850 'type': lambda *a: 'make_to_stock',
851 'product_packaging': lambda *a: False
854 def invoice_line_create(self, cr, uid, ids, context=None):
857 def _get_line_qty(line):
858 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
860 return line.product_uos_qty or 0.0
861 return line.product_uom_qty
863 return self.pool.get('procurement.order').quantity_get(cr, uid,
864 line.procurement_id.id, context=context)
866 def _get_line_uom(line):
867 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
869 return line.product_uos.id
870 return line.product_uom.id
872 return self.pool.get('procurement.order').uom_get(cr, uid,
873 line.procurement_id.id, context=context)
877 for line in self.browse(cr, uid, ids, context=context):
878 if not line.invoiced:
880 a = line.product_id.product_tmpl_id.property_account_income.id
882 a = line.product_id.categ_id.property_account_income_categ.id
884 raise osv.except_osv(_('Error !'),
885 _('There is no income account defined ' \
886 'for this product: "%s" (id:%d)') % \
887 (line.product_id.name, line.product_id.id,))
889 a = self.pool.get('ir.property').get(cr, uid,
890 'property_account_income_categ', 'product.category',
892 uosqty = _get_line_qty(line)
893 uos_id = _get_line_uom(line)
896 pu = round(line.price_unit * line.product_uom_qty / uosqty,
897 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
898 fpos = line.order_id.fiscal_position or False
899 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
901 raise osv.except_osv(_('Error !'),
902 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
903 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
905 'origin': line.order_id.name,
909 'discount': line.discount,
911 'product_id': line.product_id.id or False,
912 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
914 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
916 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
917 self.write(cr, uid, [line.id], {'invoiced': True})
919 sales[line.order_id.id] = True
920 create_ids.append(inv_id)
922 # Trigger workflow events
923 wf_service = netsvc.LocalService("workflow")
924 for sid in sales.keys():
925 wf_service.trg_write(uid, 'sale.order', sid, cr)
928 def button_cancel(self, cr, uid, ids, context=None):
931 for line in self.browse(cr, uid, ids, context=context):
933 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
934 return self.write(cr, uid, ids, {'state': 'cancel'})
936 def button_confirm(self, cr, uid, ids, context=None):
939 return self.write(cr, uid, ids, {'state': 'confirmed'})
941 def button_done(self, cr, uid, ids, context=None):
944 wf_service = netsvc.LocalService("workflow")
945 res = self.write(cr, uid, ids, {'state': 'done'})
946 for line in self.browse(cr, uid, ids, context=context):
947 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
951 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
952 product_obj = self.pool.get('product.product')
954 return {'value': {'product_uom': product_uos,
955 'product_uom_qty': product_uos_qty}, 'domain': {}}
957 product = product_obj.browse(cr, uid, product_id)
959 'product_uom': product.uom_id.id,
961 # FIXME must depend on uos/uom of the product and not only of the coeff.
964 'product_uom_qty': product_uos_qty / product.uos_coeff,
965 'th_weight': product_uos_qty / product.uos_coeff * product.weight
967 except ZeroDivisionError:
969 return {'value': value}
971 def copy_data(self, cr, uid, id, default=None, context=None):
976 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
977 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
979 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
980 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
981 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
983 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.'))
985 product_uom_obj = self.pool.get('product.uom')
986 partner_obj = self.pool.get('res.partner')
987 product_obj = self.pool.get('product.product')
989 lang = partner_obj.browse(cr, uid, partner_id).lang
990 context = {'lang': lang, 'partner_id': partner_id}
993 return {'value': {'th_weight': 0, 'product_packaging': False,
994 'product_uos_qty': qty}, 'domain': {'product_uom': [],
998 date_order = time.strftime('%Y-%m-%d')
1001 product_obj = product_obj.browse(cr, uid, product, context=context)
1002 if not packaging and product_obj.packaging:
1003 packaging = product_obj.packaging[0].id
1004 result['product_packaging'] = packaging
1007 default_uom = product_obj.uom_id and product_obj.uom_id.id
1008 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1009 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1010 # qty = qty - qty % q + q
1011 if qty and (q and not (qty % q) == 0):
1015 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)
1016 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
1018 'title': _('Picking Information !'),
1021 result['product_uom_qty'] = qty
1024 uom2 = product_uom_obj.browse(cr, uid, uom)
1025 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1029 if product_obj.uos_id:
1030 uos2 = product_uom_obj.browse(cr, uid, uos)
1031 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1035 if product_obj.description_sale:
1036 result['notes'] = product_obj.description_sale
1037 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1038 if update_tax: #The quantity only have changed
1039 result['delay'] = (product_obj.sale_delay or 0.0)
1040 partner = partner_obj.browse(cr, uid, partner_id)
1041 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1042 result.update({'type': product_obj.procure_method})
1045 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1047 if (not uom) and (not uos):
1048 result['product_uom'] = product_obj.uom_id.id
1049 if product_obj.uos_id:
1050 result['product_uos'] = product_obj.uos_id.id
1051 result['product_uos_qty'] = qty * product_obj.uos_coeff
1052 uos_category_id = product_obj.uos_id.category_id.id
1054 result['product_uos'] = False
1055 result['product_uos_qty'] = qty
1056 uos_category_id = False
1057 result['th_weight'] = qty * product_obj.weight
1058 domain = {'product_uom':
1059 [('category_id', '=', product_obj.uom_id.category_id.id)],
1061 [('category_id', '=', uos_category_id)]}
1063 elif uos and not uom: # only happens if uom is False
1064 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1065 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1066 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1067 elif uom: # whether uos is set or not
1068 default_uom = product_obj.uom_id and product_obj.uom_id.id
1069 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1070 if product_obj.uos_id:
1071 result['product_uos'] = product_obj.uos_id.id
1072 result['product_uos_qty'] = qty * product_obj.uos_coeff
1074 result['product_uos'] = False
1075 result['product_uos_qty'] = qty
1076 result['th_weight'] = q * product_obj.weight # Round the quantity up
1082 'title': 'No Pricelist !',
1084 'You have to select a pricelist or a customer in the sale form !\n'
1085 'Please set one before choosing a product.'
1088 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1089 product, qty or 1.0, partner_id, {
1095 'title': 'No valid pricelist line found !',
1097 "Couldn't find a pricelist line matching this product and quantity.\n"
1098 "You have to change either the product, the quantity or the pricelist."
1101 result.update({'price_unit': price})
1102 return {'value': result, 'domain': domain, 'warning': warning}
1104 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1105 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1106 lang=False, update_tax=True, date_order=False):
1107 res = self.product_id_change(cursor, user, ids, pricelist, product,
1108 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1109 partner_id=partner_id, lang=lang, update_tax=update_tax,
1110 date_order=date_order)
1111 if 'product_uom' in res['value']:
1112 del res['value']['product_uom']
1114 res['value']['price_unit'] = 0.0
1117 def unlink(self, cr, uid, ids, context=None):
1120 """Allows to delete sale order lines in draft,cancel states"""
1121 for rec in self.browse(cr, uid, ids, context=context):
1122 if rec.state not in ['draft', 'cancel']:
1123 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1124 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1129 class sale_config_picking_policy(osv.osv_memory):
1130 _name = 'sale.config.picking_policy'
1131 _inherit = 'res.config'
1134 'name': fields.char('Name', size=64),
1135 'picking_policy': fields.selection([
1136 ('direct', 'Direct Delivery'),
1137 ('one', 'All at Once')
1138 ], 'Picking Default Policy', required=True),
1139 'order_policy': fields.selection([
1140 ('manual', 'Invoice Based on Sales Orders'),
1141 ('picking', 'Invoice Based on Deliveries'),
1142 ], 'Shipping Default Policy', required=True,
1143 help="You can generate invoices based on sales orders or based on shippings."),
1144 'step': fields.selection([
1145 ('one', 'Delivery Order Only'),
1146 ('two', 'Picking List & Delivery Order')
1147 ], 'Steps To Deliver a Sale Order', required=True,
1148 help="By default, Open ERP is able to manage complex routing and paths "\
1149 "of products in your warehouse and partner locations. This will configure "\
1150 "the most common and simple methods to deliver products to the customer "\
1151 "in one or two operations by the worker.")
1154 'picking_policy': lambda *a: 'direct',
1155 'order_policy': lambda *a: 'manual',
1156 'step': lambda *a: 'one'
1159 def execute(self, cr, uid, ids, context=None):
1162 for o in self.browse(cr, uid, ids, context=context):
1163 ir_values_obj = self.pool.get('ir.values')
1164 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1165 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1168 md = self.pool.get('ir.model.data')
1169 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1170 group_id = md.browse(cr, uid, group_id, context=context).res_id
1171 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1172 menu_id = md.browse(cr, uid, menu_id, context=context).res_id
1173 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1175 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1176 location_id = md.browse(cr, uid, location_id, context=context).res_id
1177 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1178 sale_config_picking_policy()