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'),
46 def _incoterm_get(self, cr, uid, context={}):
47 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
51 class sale_order(osv.osv):
53 _description = "Sale Order"
55 def copy(self, cr, uid, id, default=None, context={}):
63 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
65 return super(sale_order, self).copy(cr, uid, id, default, context)
67 def _amount_line_tax(self, cr, uid, line, context={}):
69 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):
73 def _amount_all(self, cr, uid, ids, field_name, arg, context):
75 cur_obj = self.pool.get('res.currency')
76 for order in self.browse(cr, uid, ids):
78 'amount_untaxed': 0.0,
83 cur = order.pricelist_id.currency_id
84 for line in order.order_line:
85 val1 += line.price_subtotal
86 val += self._amount_line_tax(cr, uid, line, context)
87 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
88 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
89 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
92 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
99 p.sale_id,sum(m.product_qty), m.state
103 stock_picking p on (p.id=m.picking_id)
105 p.sale_id = ANY(%s) GROUP BY m.state, p.sale_id''',(ids,))
106 for oid, nbr, state in cr.fetchall():
107 if state == 'cancel':
110 res[oid][0] += nbr or 0.0
111 res[oid][1] += nbr or 0.0
113 res[oid][1] += nbr or 0.0
118 res[r] = 100.0 * res[r][0] / res[r][1]
119 for order in self.browse(cr, uid, ids, context):
121 res[order.id] = 100.0
124 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
131 for invoice in sale.invoice_ids:
132 if invoice.state not in ('draft', 'cancel'):
133 tot += invoice.amount_untaxed
136 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
141 def _invoiced(self, cursor, user, ids, name, arg, context=None):
143 for sale in self.browse(cursor, user, ids, context=context):
145 for invoice in sale.invoice_ids:
146 if invoice.state != 'paid':
149 if not sale.invoice_ids:
153 def _invoiced_search(self, cursor, user, obj, name, args, context):
161 clause += 'AND inv.state = \'paid\''
163 clause += 'AND inv.state <> \'paid\''
165 cursor.execute('SELECT rel.order_id ' \
166 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
167 'WHERE rel.invoice_id = inv.id ' + clause)
168 res = cursor.fetchall()
170 cursor.execute('SELECT sale.id ' \
171 'FROM sale_order AS sale ' \
172 'WHERE sale.id NOT IN ' \
173 '(SELECT rel.order_id ' \
174 'FROM sale_order_invoice_rel AS rel)')
175 res.extend(cursor.fetchall())
177 return [('id', '=', 0)]
178 return [('id', 'in', [x[0] for x in res])]
180 def _get_order(self, cr, uid, ids, context={}):
182 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
183 result[line.order_id.id] = True
187 'name': fields.char('Order Reference', size=64, required=True, select=True),
188 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
189 'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
190 'client_order_ref': fields.char('Customer Reference', size=64),
192 'state': fields.selection([
193 ('draft', 'Quotation'),
194 ('waiting_date', 'Waiting Schedule'),
195 ('manual', 'Manual In Progress'),
196 ('progress', 'In Progress'),
197 ('shipping_except', 'Shipping Exception'),
198 ('invoice_except', 'Invoice Exception'),
200 ('cancel', 'Cancelled')
201 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the 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),
202 'date_order': fields.date('Ordered Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
203 'create_date': fields.date('Creation Date', readonly=True),
204 'date_confirm': fields.date('Confirmation Date', readonly=True),
205 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
206 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
207 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
208 '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."),
209 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
211 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
212 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
213 '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?"""),
214 'order_policy': fields.selection([
215 ('prepaid', 'Payment Before Delivery'),
216 ('manual', 'Shipping & Manual Invoice'),
217 ('postpaid', 'Invoice on Order After Delivery'),
218 ('picking', 'Invoice from Picking'),
219 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
220 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
221 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
222 - 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.
223 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
224 - The 'Invoice from the picking' choice is used to create an invoice during the picking process."""),
225 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
226 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
228 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
229 '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)."),
230 '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"),
231 'shipped': fields.boolean('Picked', readonly=True),
232 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
233 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
234 'invoiced': fields.function(_invoiced, method=True, string='Paid',
235 fnct_search=_invoiced_search, type='boolean'),
236 'note': fields.text('Notes', translate=True),
238 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
240 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
241 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
244 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
246 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
247 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
250 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Total',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
257 '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),
258 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
259 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
260 'company_id': fields.many2one('res.company','Company',select=1),
263 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', context=c),
264 'picking_policy': lambda *a: 'direct',
265 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
266 'order_policy': lambda *a: 'manual',
267 'state': lambda *a: 'draft',
268 'user_id': lambda obj, cr, uid, context: uid,
269 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
270 'invoice_quantity': lambda *a: 'order',
271 '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'],
272 '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'],
273 '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'],
274 '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,
279 def unlink(self, cr, uid, ids, context=None):
280 sale_orders = self.read(cr, uid, ids, ['state'])
282 for s in sale_orders:
283 if s['state'] in ['draft', 'cancel']:
284 unlink_ids.append(s['id'])
286 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
287 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
289 def onchange_shop_id(self, cr, uid, ids, shop_id):
292 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
293 v['project_id'] = shop.project_id.id
294 # Que faire si le client a une pricelist a lui ?
295 if shop.pricelist_id.id:
296 v['pricelist_id'] = shop.pricelist_id.id
297 #v['payment_default_id']=shop.payment_default_id.id
300 def action_cancel_draft(self, cr, uid, ids, *args):
303 cr.execute('select id from sale_order_line where order_id = ANY(%s) and state=%s',(ids,'cancel'))
304 line_ids = map(lambda x: x[0], cr.fetchall())
305 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
306 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
307 wf_service = netsvc.LocalService("workflow")
309 # Deleting the existing instance of workflow for SO
310 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
311 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
314 def onchange_partner_id(self, cr, uid, ids, part):
316 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
318 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
319 part = self.pool.get('res.partner').browse(cr, uid, part)
320 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
321 payment_term = part.property_payment_term and part.property_payment_term.id or False
322 fiscal_position = part.property_account_position and part.property_account_position.id or False
323 dedicated_salesman = part.user_id and part.user_id.id or uid
326 'partner_invoice_id': addr['invoice'],
327 'partner_order_id': addr['contact'],
328 'partner_shipping_id': addr['delivery'],
329 'payment_term': payment_term,
330 'fiscal_position': fiscal_position,
331 'user_id': dedicated_salesman,
335 val['pricelist_id'] = pricelist
337 return {'value': val}
339 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
343 if policy == 'prepaid':
345 elif policy == 'picking':
346 inv_qty = 'procurement'
347 return {'value': {'invoice_quantity': inv_qty}}
349 def write(self, cr, uid, ids, vals, context=None):
350 if 'order_policy' in vals:
351 if vals['order_policy'] == 'prepaid':
352 vals.update({'invoice_quantity': 'order'})
353 elif vals['order_policy'] == 'picking':
354 vals.update({'invoice_quantity': 'procurement'})
355 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
357 def create(self, cr, uid, vals, context={}):
358 if 'order_policy' in vals:
359 if vals['order_policy'] == 'prepaid':
360 vals.update({'invoice_quantity': 'order'})
361 if vals['order_policy'] == 'picking':
362 vals.update({'invoice_quantity': 'procurement'})
363 return super(sale_order, self).create(cr, uid, vals, context=context)
365 def button_dummy(self, cr, uid, ids, context={}):
368 #FIXME: the method should return the list of invoices created (invoice_ids)
369 # and not the id of the last invoice created (res). The problem is that we
370 # cannot change it directly since the method is called by the sale order
371 # workflow and I suppose it expects a single id...
372 def _inv_get(self, cr, uid, order, context={}):
375 def _make_invoice(self, cr, uid, order, lines, context={}):
376 a = order.partner_id.property_account_receivable.id
377 if order.payment_term:
378 pay_term = order.payment_term.id
381 for preinv in order.invoice_ids:
382 if preinv.state not in ('cancel',):
383 for preline in preinv.invoice_line:
384 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
385 lines.append(inv_line_id)
386 journal_obj = self.pool.get('account.journal')
387 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
389 raise osv.except_osv(_('Error !'),
390 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
392 'name': order.client_order_ref or order.name,
393 'origin': order.name,
394 'type': 'out_invoice',
395 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
397 'partner_id': order.partner_id.id,
398 'journal_id': journal_ids[0],
399 'address_invoice_id': order.partner_invoice_id.id,
400 'address_contact_id': order.partner_order_id.id,
401 'invoice_line': [(6, 0, lines)],
402 'currency_id': order.pricelist_id.currency_id.id,
403 'comment': order.note,
404 'payment_term': pay_term,
405 'fiscal_position': order.partner_id.property_account_position.id,
406 'date_invoice' : context.get('date_invoice',False),
407 'company_id' : order.company_id.id,
409 inv_obj = self.pool.get('account.invoice')
410 inv.update(self._inv_get(cr, uid, order))
411 inv_id = inv_obj.create(cr, uid, inv, context)
412 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
413 if data.get('value', False):
414 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
415 inv_obj.button_compute(cr, uid, [inv_id])
418 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
424 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
425 # last day of the last month as invoice date
427 context['date_inv'] = date_inv
428 for o in self.browse(cr, uid, ids):
430 for line in o.order_line:
431 if (line.state in states) and not line.invoiced:
432 lines.append(line.id)
433 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
435 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
438 for o in self.browse(cr, uid, ids):
439 for i in o.invoice_ids:
440 if i.state == 'draft':
442 picking_obj = self.pool.get('stock.picking')
443 for val in invoices.values():
445 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
447 self.write(cr, uid, [o.id], {'state': 'progress'})
448 if o.order_policy == 'picking':
449 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
450 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
452 for order, il in val:
453 res = self._make_invoice(cr, uid, order, il, context=context)
454 invoice_ids.append(res)
455 self.write(cr, uid, [order.id], {'state': 'progress'})
456 if order.order_policy == 'picking':
457 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
458 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
461 def action_invoice_cancel(self, cr, uid, ids, context={}):
462 for sale in self.browse(cr, uid, ids):
463 for line in sale.order_line:
465 for iline in line.invoice_lines:
466 if iline.invoice_id and iline.invoice_id.state == 'cancel':
470 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
471 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
474 def action_invoice_end(self, cr, uid, ids, context={}):
475 for order in self.browse(cr, uid, ids):
476 val = {'invoiced': True}
477 if order.state == 'invoice_except':
478 val['state'] = 'progress'
480 for line in order.order_line:
482 if line.state == 'exception':
483 towrite.append(line.id)
485 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'confirmed'}, context=context)
486 self.write(cr, uid, [order.id], val)
490 def action_cancel(self, cr, uid, ids, context={}):
492 sale_order_line_obj = self.pool.get('sale.order.line')
493 for sale in self.browse(cr, uid, ids):
494 for pick in sale.picking_ids:
495 if pick.state not in ('draft', 'cancel'):
496 raise osv.except_osv(
497 _('Could not cancel sale order !'),
498 _('You must first cancel all picking attached to this sale order.'))
499 for r in self.read(cr, uid, ids, ['picking_ids']):
500 for pick in r['picking_ids']:
501 wf_service = netsvc.LocalService("workflow")
502 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
503 for inv in sale.invoice_ids:
504 if inv.state not in ('draft', 'cancel'):
505 raise osv.except_osv(
506 _('Could not cancel this sale order !'),
507 _('You must first cancel all invoices attached to this sale order.'))
508 for r in self.read(cr, uid, ids, ['invoice_ids']):
509 for inv in r['invoice_ids']:
510 wf_service = netsvc.LocalService("workflow")
511 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
512 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
514 self.write(cr, uid, ids, {'state': 'cancel'})
517 def action_wait(self, cr, uid, ids, *args):
518 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
519 event_obj = self.pool.get('res.partner.event')
520 for o in self.browse(cr, uid, ids):
522 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
523 'partner_id': o.partner_id.id,\
524 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
525 'user_id': (o.user_id and o.user_id.id) or uid,\
526 'partner_type': 'customer', 'probability': 1.0,\
527 'planned_revenue': o.amount_untaxed})
528 if (o.order_policy == 'manual'):
529 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
531 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
532 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
534 def procurement_lines_get(self, cr, uid, ids, *args):
536 for order in self.browse(cr, uid, ids, context={}):
537 for line in order.order_line:
538 if line.procurement_id:
539 res.append(line.procurement_id.id)
542 # if mode == 'finished':
543 # returns True if all lines are done, False otherwise
544 # if mode == 'canceled':
545 # returns True if there is at least one canceled line, False otherwise
546 def test_state(self, cr, uid, ids, mode, *args):
547 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
552 write_cancel_ids = []
553 for order in self.browse(cr, uid, ids, context={}):
554 for line in order.order_line:
555 if (not line.procurement_id) or (line.procurement_id.state=='done'):
556 if line.state != 'done':
557 write_done_ids.append(line.id)
560 if line.procurement_id:
561 if (line.procurement_id.state == 'cancel'):
563 if line.state != 'exception':
564 write_cancel_ids.append(line.id)
568 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
570 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
572 if mode == 'finished':
574 elif mode == 'canceled':
580 def action_ship_create(self, cr, uid, ids, *args):
582 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
583 for order in self.browse(cr, uid, ids, context={}):
584 output_id = order.shop_id.warehouse_id.lot_output_id.id
586 for line in order.order_line:
588 date_planned = datetime.now() + relativedelta(days=line.delay or 0.0)
589 date_planned = (date_planned - relativedelta(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
590 if line.state == 'done':
592 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
593 location_id = order.shop_id.warehouse_id.lot_stock_id.id
595 loc_dest_id = order.partner_id.property_stock_customer.id
596 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
597 picking_id = self.pool.get('stock.picking').create(cr, uid, {
599 'origin': order.name,
602 'move_type': order.picking_policy,
604 'address_id': order.partner_shipping_id.id,
606 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
607 'company_id': order.company_id.id,
610 move_id = self.pool.get('stock.move').create(cr, uid, {
611 'name': line.name[:64],
612 'picking_id': picking_id,
613 'product_id': line.product_id.id,
614 'date_planned': date_planned,
615 'product_qty': line.product_uom_qty,
616 'product_uom': line.product_uom.id,
617 'product_uos_qty': line.product_uos_qty,
618 'product_uos': (line.product_uos and line.product_uos.id)\
619 or line.product_uom.id,
620 'product_packaging': line.product_packaging.id,
621 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
622 'location_id': location_id,
623 'location_dest_id': output_id,
624 'sale_line_id': line.id,
625 'tracking_id': False,
629 'company_id': order.company_id.id,
631 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
633 'origin': order.name,
634 'date_planned': date_planned,
635 'product_id': line.product_id.id,
636 'product_qty': line.product_uom_qty,
637 'product_uom': line.product_uom.id,
638 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
639 or line.product_uom_qty,
640 'product_uos': (line.product_uos and line.product_uos.id)\
641 or line.product_uom.id,
642 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
643 'procure_method': line.type,
645 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
646 'company_id': order.company_id.id,
648 wf_service = netsvc.LocalService("workflow")
649 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
650 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
651 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
652 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
654 'origin': order.name,
655 'date_planned': date_planned,
656 'product_id': line.product_id.id,
657 'product_qty': line.product_uom_qty,
658 'product_uom': line.product_uom.id,
659 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
660 'procure_method': line.type,
661 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
662 'company_id': order.company_id.id,
664 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
665 wf_service = netsvc.LocalService("workflow")
666 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
669 # No procurement because no product in the sale.order.line.
675 wf_service = netsvc.LocalService("workflow")
676 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
678 if order.state == 'shipping_except':
679 val['state'] = 'progress'
681 if (order.order_policy == 'manual'):
682 for line in order.order_line:
683 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
684 val['state'] = 'manual'
686 self.write(cr, uid, [order.id], val)
690 def action_ship_end(self, cr, uid, ids, context={}):
691 for order in self.browse(cr, uid, ids):
692 val = {'shipped': True}
693 if order.state == 'shipping_except':
694 val['state'] = 'progress'
695 if (order.order_policy == 'manual'):
696 for line in order.order_line:
697 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
698 val['state'] = 'manual'
700 for line in order.order_line:
702 if line.state == 'exception':
703 towrite.append(line.id)
705 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
706 self.write(cr, uid, [order.id], val)
709 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
710 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
712 part = inv['partner_id'] and inv['partner_id'][0]
713 pr = inv['amount_untaxed'] or 0.0
714 partnertype = 'customer'
717 'name': 'Order: '+name,
719 'description': 'Order '+str(inv['id']),
722 'date': time.strftime('%Y-%m-%d'),
725 'partner_type': partnertype,
727 'planned_revenue': pr,
731 self.pool.get('res.partner.event').create(cr, uid, event)
733 def has_stockable_products(self, cr, uid, ids, *args):
734 for order in self.browse(cr, uid, ids):
735 for order_line in order.order_line:
736 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
741 # TODO add a field price_unit_uos
742 # - update it on change product and unit price
743 # - use it in report if there is a uos
744 class sale_order_line(osv.osv):
745 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
747 for line in self.browse(cr, uid, ids):
748 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
751 def _amount_line(self, cr, uid, ids, field_name, arg, context):
753 cur_obj = self.pool.get('res.currency')
754 for line in self.browse(cr, uid, ids):
755 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
756 cur = line.order_id.pricelist_id.currency_id
757 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
760 def _number_packages(self, cr, uid, ids, field_name, arg, context):
762 for line in self.browse(cr, uid, ids):
764 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
769 _name = 'sale.order.line'
770 _description = 'Sale Order line'
772 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
773 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
774 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
775 '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)]}),
776 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
777 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
778 'invoiced': fields.boolean('Invoiced', readonly=True),
779 'procurement_id': fields.many2one('mrp.procurement', 'Requisition'),
780 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft':[('readonly',False)]}),
781 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits_compute= dp.get_precision('Sale Price')),
782 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
783 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
784 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Requisition Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
785 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
786 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
787 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
788 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}),
789 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
790 'product_uos': fields.many2one('product.uom', 'Product UoS'),
791 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
792 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
793 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
794 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
795 'notes': fields.text('Notes', translate=True),
796 'th_weight': fields.float('Weight'),
797 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
798 help=' * The \'Draft\' state is set automatically when sale order in draft state. \
799 \n* The \'Confirmed\' state is set automatically when sale order in confirm state. \
800 \n* The \'Exception\' state is set automatically when sale order is set as exception. \
801 \n* The \'Done\' state is set automatically when sale order is set as done. \
802 \n* The \'Cancelled\' state is set automatically when user cancel sale order.'),
803 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
804 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
805 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
807 _order = 'sequence, id'
809 'discount': lambda *a: 0.0,
810 'delay': lambda *a: 0.0,
811 'product_uom_qty': lambda *a: 1,
812 'product_uos_qty': lambda *a: 1,
813 'sequence': lambda *a: 10,
814 'invoiced': lambda *a: 0,
815 'state': lambda *a: 'draft',
816 'type': lambda *a: 'make_to_stock',
817 'product_packaging': lambda *a: False
820 def invoice_line_create(self, cr, uid, ids, context={}):
821 def _get_line_qty(line):
822 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
824 return line.product_uos_qty or 0.0
825 return line.product_uom_qty
827 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
828 line.procurement_id.id, context)
830 def _get_line_uom(line):
831 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
833 return line.product_uos.id
834 return line.product_uom.id
836 return self.pool.get('mrp.procurement').uom_get(cr, uid,
837 line.procurement_id.id, context)
841 for line in self.browse(cr, uid, ids, context):
842 if not line.invoiced:
844 a = line.product_id.product_tmpl_id.property_account_income.id
846 a = line.product_id.categ_id.property_account_income_categ.id
848 raise osv.except_osv(_('Error !'),
849 _('There is no income account defined ' \
850 'for this product: "%s" (id:%d)') % \
851 (line.product_id.name, line.product_id.id,))
853 a = self.pool.get('ir.property').get(cr, uid,
854 'property_account_income_categ', 'product.category',
856 uosqty = _get_line_qty(line)
857 uos_id = _get_line_uom(line)
860 pu = round(line.price_unit * line.product_uom_qty / uosqty,
861 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
862 fpos = line.order_id.fiscal_position or False
863 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
865 raise osv.except_osv(_('Error !'),
866 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
867 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
869 'origin': line.order_id.name,
873 'discount': line.discount,
875 'product_id': line.product_id.id or False,
876 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
878 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
880 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
881 self.write(cr, uid, [line.id], {'invoiced': True})
883 sales[line.order_id.id] = True
884 create_ids.append(inv_id)
886 # Trigger workflow events
887 wf_service = netsvc.LocalService("workflow")
888 for sid in sales.keys():
889 wf_service.trg_write(uid, 'sale.order', sid, cr)
892 def button_cancel(self, cr, uid, ids, context={}):
893 for line in self.browse(cr, uid, ids, context=context):
895 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
896 return self.write(cr, uid, ids, {'state': 'cancel'})
898 def button_confirm(self, cr, uid, ids, context={}):
899 return self.write(cr, uid, ids, {'state': 'confirmed'})
901 def button_done(self, cr, uid, ids, context={}):
902 wf_service = netsvc.LocalService("workflow")
903 res = self.write(cr, uid, ids, {'state': 'done'})
904 for line in self.browse(cr, uid, ids, context):
905 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
909 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
910 product_obj = self.pool.get('product.product')
912 return {'value': {'product_uom': product_uos,
913 'product_uom_qty': product_uos_qty}, 'domain': {}}
915 product = product_obj.browse(cr, uid, product_id)
917 'product_uom': product.uom_id.id,
919 # FIXME must depend on uos/uom of the product and not only of the coeff.
922 'product_uom_qty': product_uos_qty / product.uos_coeff,
923 'th_weight': product_uos_qty / product.uos_coeff * product.weight
925 except ZeroDivisionError:
927 return {'value': value}
929 def copy_data(self, cr, uid, id, default=None, context={}):
932 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
933 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
935 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
936 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
937 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
939 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.'))
941 product_uom_obj = self.pool.get('product.uom')
942 partner_obj = self.pool.get('res.partner')
943 product_obj = self.pool.get('product.product')
945 lang = partner_obj.browse(cr, uid, partner_id).lang
946 context = {'lang': lang, 'partner_id': partner_id}
949 return {'value': {'th_weight': 0, 'product_packaging': False,
950 'product_uos_qty': qty}, 'domain': {'product_uom': [],
954 date_order = time.strftime('%Y-%m-%d')
957 product_obj = product_obj.browse(cr, uid, product, context=context)
958 if not packaging and product_obj.packaging:
959 packaging = product_obj.packaging[0].id
960 result['product_packaging'] = packaging
963 default_uom = product_obj.uom_id and product_obj.uom_id.id
964 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
965 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
966 # qty = qty - qty % q + q
967 if qty and (q and not (qty % q) == 0):
971 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)
972 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
974 'title': _('Picking Information !'),
977 result['product_uom_qty'] = qty
980 uom2 = product_uom_obj.browse(cr, uid, uom)
981 if product_obj.uom_id.category_id.id != uom2.category_id.id:
985 if product_obj.uos_id:
986 uos2 = product_uom_obj.browse(cr, uid, uos)
987 if product_obj.uos_id.category_id.id != uos2.category_id.id:
991 if product_obj.description_sale:
992 result['notes'] = product_obj.description_sale
993 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
994 if update_tax: #The quantity only have changed
995 result['delay'] = (product_obj.sale_delay or 0.0)
996 partner = partner_obj.browse(cr, uid, partner_id)
997 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
998 result.update({'type': product_obj.procure_method})
1001 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1003 if (not uom) and (not uos):
1004 result['product_uom'] = product_obj.uom_id.id
1005 if product_obj.uos_id:
1006 result['product_uos'] = product_obj.uos_id.id
1007 result['product_uos_qty'] = qty * product_obj.uos_coeff
1008 uos_category_id = product_obj.uos_id.category_id.id
1010 result['product_uos'] = False
1011 result['product_uos_qty'] = qty
1012 uos_category_id = False
1013 result['th_weight'] = qty * product_obj.weight
1014 domain = {'product_uom':
1015 [('category_id', '=', product_obj.uom_id.category_id.id)],
1017 [('category_id', '=', uos_category_id)]}
1019 elif uos: # only happens if uom is False
1020 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1021 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1022 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1023 elif uom: # whether uos is set or not
1024 default_uom = product_obj.uom_id and product_obj.uom_id.id
1025 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1026 if product_obj.uos_id:
1027 result['product_uos'] = product_obj.uos_id.id
1028 result['product_uos_qty'] = qty * product_obj.uos_coeff
1030 result['product_uos'] = False
1031 result['product_uos_qty'] = qty
1032 result['th_weight'] = q * product_obj.weight # Round the quantity up
1038 'title': 'No Pricelist !',
1040 'You have to select a pricelist in the sale form !\n'
1041 'Please set one before choosing a product.'
1044 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1045 product, qty or 1.0, partner_id, {
1051 'title': 'No valid pricelist line found !',
1053 "Couldn't find a pricelist line matching this product and quantity.\n"
1054 "You have to change either the product, the quantity or the pricelist."
1057 result.update({'price_unit': price})
1058 return {'value': result, 'domain': domain, 'warning': warning}
1060 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1061 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1062 lang=False, update_tax=True, date_order=False):
1063 res = self.product_id_change(cursor, user, ids, pricelist, product,
1064 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1065 partner_id=partner_id, lang=lang, update_tax=update_tax,
1066 date_order=date_order)
1067 if 'product_uom' in res['value']:
1068 del res['value']['product_uom']
1070 res['value']['price_unit'] = 0.0
1073 def unlink(self, cr, uid, ids, context={}):
1074 """Allows to delete sale order lines in draft,cancel states"""
1075 for rec in self.browse(cr, uid, ids, context=context):
1076 if rec.state not in ['draft', 'cancel']:
1077 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1078 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1083 class sale_config_picking_policy(osv.osv_memory):
1084 _name = 'sale.config.picking_policy'
1085 _inherit = 'res.config'
1088 'name': fields.char('Name', size=64),
1089 'picking_policy': fields.selection([
1090 ('direct', 'Direct Delivery'),
1091 ('one', 'All at Once')
1092 ], 'Picking Default Policy', required=True),
1093 'order_policy': fields.selection([
1094 ('manual', 'Invoice Based on Sales Orders'),
1095 ('picking', 'Invoice Based on Deliveries'),
1096 ], 'Shipping Default Policy', required=True),
1097 'step': fields.selection([
1098 ('one', 'Delivery Order Only'),
1099 ('two', 'Picking List & Delivery Order')
1100 ], 'Steps To Deliver a Sale Order', required=True,
1101 help="By default, Open ERP is able to manage complex routing and paths "\
1102 "of products in your warehouse and partner locations. This will configure "\
1103 "the most common and simple methods to deliver products to the customer "\
1104 "in one or two operations by the worker.")
1107 'picking_policy': lambda *a: 'direct',
1108 'order_policy': lambda *a: 'picking',
1109 'step': lambda *a: 'one'
1112 def execute(self, cr, uid, ids, context=None):
1113 for o in self.browse(cr, uid, ids, context=context):
1114 ir_values_obj = self.pool.get('ir.values')
1115 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1116 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1119 md = self.pool.get('ir.model.data')
1120 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1121 group_id = md.browse(cr, uid, group_id, context).res_id
1122 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1123 menu_id = md.browse(cr, uid, menu_id, context).res_id
1124 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1126 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1127 location_id = md.browse(cr, uid, location_id, context).res_id
1128 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1129 sale_config_picking_policy()