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 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 = ANY(%s) GROUP BY m.state, p.sale_id''',(ids,))
103 for oid, nbr, state in cr.fetchall():
104 if state == 'cancel':
107 res[oid][0] += nbr or 0.0
108 res[oid][1] += nbr or 0.0
110 res[oid][1] += nbr or 0.0
115 res[r] = 100.0 * res[r][0] / res[r][1]
116 for order in self.browse(cr, uid, ids, context):
118 res[order.id] = 100.0
121 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
123 for sale in self.browse(cursor, user, ids, context=context):
128 for invoice in sale.invoice_ids:
129 if invoice.state not in ('draft', 'cancel'):
130 tot += invoice.amount_untaxed
133 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
138 def _invoiced(self, cursor, user, ids, name, arg, context=None):
140 for sale in self.browse(cursor, user, ids, context=context):
142 for invoice in sale.invoice_ids:
143 if invoice.state != 'paid':
146 if not sale.invoice_ids:
150 def _invoiced_search(self, cursor, user, obj, name, args, context):
158 clause += 'AND inv.state = \'paid\''
160 clause += 'AND inv.state <> \'paid\''
162 cursor.execute('SELECT rel.order_id ' \
163 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
164 'WHERE rel.invoice_id = inv.id ' + clause)
165 res = cursor.fetchall()
167 cursor.execute('SELECT sale.id ' \
168 'FROM sale_order AS sale ' \
169 'WHERE sale.id NOT IN ' \
170 '(SELECT rel.order_id ' \
171 'FROM sale_order_invoice_rel AS rel)')
172 res.extend(cursor.fetchall())
174 return [('id', '=', 0)]
175 return [('id', 'in', [x[0] for x in res])]
177 def _get_order(self, cr, uid, ids, context={}):
179 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
180 result[line.order_id.id] = True
184 'name': fields.char('Order Reference', size=64, required=True, select=True),
185 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
186 'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
187 'client_order_ref': fields.char('Customer Reference', size=64),
189 'state': fields.selection([
190 ('draft', 'Quotation'),
191 ('waiting_date', 'Waiting Schedule'),
192 ('manual', 'Manual In Progress'),
193 ('progress', 'In Progress'),
194 ('shipping_except', 'Shipping Exception'),
195 ('invoice_except', 'Invoice Exception'),
197 ('cancel', 'Cancelled')
198 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Date Ordered'.", select=True),
199 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
201 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
202 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
203 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
204 '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."),
205 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
207 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
208 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
209 '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?"""),
210 'order_policy': fields.selection([
211 ('prepaid', 'Payment Before Delivery'),
212 ('manual', 'Shipping & Manual Invoice'),
213 ('postpaid', 'Invoice on Order After Delivery'),
214 ('picking', 'Invoice from Picking'),
215 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
216 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
217 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
218 - 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.
219 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
220 - The 'Invoice from the picking' choice is used to create an invoice during the picking process."""),
221 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
222 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
224 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
225 '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)."),
226 '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"),
227 'shipped': fields.boolean('Picked', readonly=True),
228 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
229 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
230 'invoiced': fields.function(_invoiced, method=True, string='Paid',
231 fnct_search=_invoiced_search, type='boolean'),
232 'note': fields.text('Notes', translate=True),
234 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
236 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
237 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
242 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
243 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
246 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
248 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
249 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
253 '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),
254 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
255 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
256 'company_id': fields.many2one('res.company','Company',select=1),
259 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', context=c),
260 'picking_policy': lambda *a: 'direct',
261 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
262 'order_policy': lambda *a: 'manual',
263 'state': lambda *a: 'draft',
264 'user_id': lambda obj, cr, uid, context: uid,
265 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
266 'invoice_quantity': lambda *a: 'order',
267 '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'],
268 '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'],
269 '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'],
270 '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,
275 def unlink(self, cr, uid, ids, context=None):
276 sale_orders = self.read(cr, uid, ids, ['state'])
278 for s in sale_orders:
279 if s['state'] in ['draft', 'cancel']:
280 unlink_ids.append(s['id'])
282 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
283 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
285 def onchange_shop_id(self, cr, uid, ids, shop_id):
288 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
289 v['project_id'] = shop.project_id.id
290 # Que faire si le client a une pricelist a lui ?
291 if shop.pricelist_id.id:
292 v['pricelist_id'] = shop.pricelist_id.id
293 #v['payment_default_id']=shop.payment_default_id.id
296 def action_cancel_draft(self, cr, uid, ids, *args):
299 cr.execute('select id from sale_order_line where order_id = ANY(%s) and state=%s',(ids,'cancel'))
300 line_ids = map(lambda x: x[0], cr.fetchall())
301 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
302 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
303 wf_service = netsvc.LocalService("workflow")
305 # Deleting the existing instance of workflow for SO
306 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
307 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
310 def onchange_partner_id(self, cr, uid, ids, part):
312 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
314 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
315 part = self.pool.get('res.partner').browse(cr, uid, part)
316 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
317 payment_term = part.property_payment_term and part.property_payment_term.id or False
318 fiscal_position = part.property_account_position and part.property_account_position.id or False
319 dedicated_salesman = part.user_id and part.user_id.id or uid
322 'partner_invoice_id': addr['invoice'],
323 'partner_order_id': addr['contact'],
324 'partner_shipping_id': addr['delivery'],
325 'payment_term': payment_term,
326 'fiscal_position': fiscal_position,
327 'user_id': dedicated_salesman,
331 val['pricelist_id'] = pricelist
333 return {'value': val}
335 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
339 if policy == 'prepaid':
341 elif policy == 'picking':
342 inv_qty = 'procurement'
343 return {'value': {'invoice_quantity': inv_qty}}
345 def write(self, cr, uid, ids, vals, context=None):
346 if 'order_policy' in vals:
347 if vals['order_policy'] == 'prepaid':
348 vals.update({'invoice_quantity': 'order'})
349 elif vals['order_policy'] == 'picking':
350 vals.update({'invoice_quantity': 'procurement'})
351 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
353 def create(self, cr, uid, vals, context={}):
354 if 'order_policy' in vals:
355 if vals['order_policy'] == 'prepaid':
356 vals.update({'invoice_quantity': 'order'})
357 if vals['order_policy'] == 'picking':
358 vals.update({'invoice_quantity': 'procurement'})
359 return super(sale_order, self).create(cr, uid, vals, context=context)
361 def button_dummy(self, cr, uid, ids, context={}):
364 #FIXME: the method should return the list of invoices created (invoice_ids)
365 # and not the id of the last invoice created (res). The problem is that we
366 # cannot change it directly since the method is called by the sale order
367 # workflow and I suppose it expects a single id...
368 def _inv_get(self, cr, uid, order, context={}):
371 def _make_invoice(self, cr, uid, order, lines, context={}):
372 a = order.partner_id.property_account_receivable.id
373 if order.payment_term:
374 pay_term = order.payment_term.id
377 for preinv in order.invoice_ids:
378 if preinv.state not in ('cancel',):
379 for preline in preinv.invoice_line:
380 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
381 lines.append(inv_line_id)
382 journal_obj = self.pool.get('account.journal')
383 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
385 raise osv.except_osv(_('Error !'),
386 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
388 'name': order.client_order_ref or order.name,
389 'origin': order.name,
390 'type': 'out_invoice',
391 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
393 'partner_id': order.partner_id.id,
394 'journal_id': journal_ids[0],
395 'address_invoice_id': order.partner_invoice_id.id,
396 'address_contact_id': order.partner_order_id.id,
397 'invoice_line': [(6, 0, lines)],
398 'currency_id': order.pricelist_id.currency_id.id,
399 'comment': order.note,
400 'payment_term': pay_term,
401 'fiscal_position': order.partner_id.property_account_position.id,
402 'date_invoice' : context.get('date_invoice',False),
403 'company_id' : order.company_id.id,
405 inv_obj = self.pool.get('account.invoice')
406 inv.update(self._inv_get(cr, uid, order))
407 inv_id = inv_obj.create(cr, uid, inv, context)
408 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
409 if data.get('value', False):
410 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
411 inv_obj.button_compute(cr, uid, [inv_id])
414 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
420 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
421 # last day of the last month as invoice date
423 context['date_inv'] = date_inv
424 for o in self.browse(cr, uid, ids):
426 for line in o.order_line:
427 if (line.state in states) and not line.invoiced:
428 lines.append(line.id)
429 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
431 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
434 for o in self.browse(cr, uid, ids):
435 for i in o.invoice_ids:
436 if i.state == 'draft':
438 picking_obj = self.pool.get('stock.picking')
439 for val in invoices.values():
441 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
443 self.write(cr, uid, [o.id], {'state': 'progress'})
444 if o.order_policy == 'picking':
445 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
446 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
448 for order, il in val:
449 res = self._make_invoice(cr, uid, order, il, context=context)
450 invoice_ids.append(res)
451 self.write(cr, uid, [order.id], {'state': 'progress'})
452 if order.order_policy == 'picking':
453 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
454 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
457 def action_invoice_cancel(self, cr, uid, ids, context={}):
458 for sale in self.browse(cr, uid, ids):
459 for line in sale.order_line:
461 for iline in line.invoice_lines:
462 if iline.invoice_id and iline.invoice_id.state == 'cancel':
466 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
467 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
470 def action_invoice_end(self, cr, uid, ids, context={}):
471 for order in self.browse(cr, uid, ids):
472 val = {'invoiced': True}
473 if order.state == 'invoice_except':
474 val['state'] = 'progress'
476 for line in order.order_line:
478 if line.state == 'exception':
479 towrite.append(line.id)
481 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'confirmed'}, context=context)
482 self.write(cr, uid, [order.id], val)
486 def action_cancel(self, cr, uid, ids, context={}):
488 sale_order_line_obj = self.pool.get('sale.order.line')
489 for sale in self.browse(cr, uid, ids):
490 for pick in sale.picking_ids:
491 if pick.state not in ('draft', 'cancel'):
492 raise osv.except_osv(
493 _('Could not cancel sale order !'),
494 _('You must first cancel all picking attached to this sale order.'))
495 for r in self.read(cr, uid, ids, ['picking_ids']):
496 for pick in r['picking_ids']:
497 wf_service = netsvc.LocalService("workflow")
498 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
499 for inv in sale.invoice_ids:
500 if inv.state not in ('draft', 'cancel'):
501 raise osv.except_osv(
502 _('Could not cancel this sale order !'),
503 _('You must first cancel all invoices attached to this sale order.'))
504 for r in self.read(cr, uid, ids, ['invoice_ids']):
505 for inv in r['invoice_ids']:
506 wf_service = netsvc.LocalService("workflow")
507 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
508 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
510 self.write(cr, uid, ids, {'state': 'cancel'})
513 def action_wait(self, cr, uid, ids, *args):
514 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
515 event_obj = self.pool.get('res.partner.event')
516 for o in self.browse(cr, uid, ids):
518 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
519 'partner_id': o.partner_id.id,\
520 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
521 'user_id': (o.user_id and o.user_id.id) or uid,\
522 'partner_type': 'customer', 'probability': 1.0,\
523 'planned_revenue': o.amount_untaxed})
524 if (o.order_policy == 'manual'):
525 self.write(cr, uid, [o.id], {'state': 'manual'})
527 self.write(cr, uid, [o.id], {'state': 'progress'})
528 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
530 def procurement_lines_get(self, cr, uid, ids, *args):
532 for order in self.browse(cr, uid, ids, context={}):
533 for line in order.order_line:
534 if line.procurement_id:
535 res.append(line.procurement_id.id)
538 # if mode == 'finished':
539 # returns True if all lines are done, False otherwise
540 # if mode == 'canceled':
541 # returns True if there is at least one canceled line, False otherwise
542 def test_state(self, cr, uid, ids, mode, *args):
543 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
548 write_cancel_ids = []
549 for order in self.browse(cr, uid, ids, context={}):
550 for line in order.order_line:
551 if (not line.procurement_id) or (line.procurement_id.state=='done'):
552 if line.state != 'done':
553 write_done_ids.append(line.id)
556 if line.procurement_id:
557 if (line.procurement_id.state == 'cancel'):
559 if line.state != 'exception':
560 write_cancel_ids.append(line.id)
564 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
566 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
568 if mode == 'finished':
570 elif mode == 'canceled':
576 def action_ship_create(self, cr, uid, ids, *args):
578 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
579 for order in self.browse(cr, uid, ids, context={}):
580 output_id = order.shop_id.warehouse_id.lot_output_id.id
582 for line in order.order_line:
584 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
585 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
586 if line.state == 'done':
588 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
589 location_id = order.shop_id.warehouse_id.lot_stock_id.id
591 loc_dest_id = order.partner_id.property_stock_customer.id
592 picking_id = self.pool.get('stock.picking').create(cr, uid, {
593 'origin': order.name,
596 'move_type': order.picking_policy,
598 'address_id': order.partner_shipping_id.id,
600 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
601 'company_id': order.company_id.id,
604 move_id = self.pool.get('stock.move').create(cr, uid, {
605 'name': line.name[:64],
606 'picking_id': picking_id,
607 'product_id': line.product_id.id,
608 'date_planned': date_planned,
609 'product_qty': line.product_uom_qty,
610 'product_uom': line.product_uom.id,
611 'product_uos_qty': line.product_uos_qty,
612 'product_uos': (line.product_uos and line.product_uos.id)\
613 or line.product_uom.id,
614 'product_packaging': line.product_packaging.id,
615 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
616 'location_id': location_id,
617 'location_dest_id': output_id,
618 'sale_line_id': line.id,
619 'tracking_id': False,
623 'company_id': order.company_id.id,
625 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
627 'origin': order.name,
628 'date_planned': date_planned,
629 'product_id': line.product_id.id,
630 'product_qty': line.product_uom_qty,
631 'product_uom': line.product_uom.id,
632 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
633 or line.product_uom_qty,
634 'product_uos': (line.product_uos and line.product_uos.id)\
635 or line.product_uom.id,
636 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
637 'procure_method': line.type,
639 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
640 'company_id': order.company_id.id,
642 wf_service = netsvc.LocalService("workflow")
643 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
644 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
645 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
646 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
648 'origin': order.name,
649 'date_planned': date_planned,
650 'product_id': line.product_id.id,
651 'product_qty': line.product_uom_qty,
652 'product_uom': line.product_uom.id,
653 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
654 'procure_method': line.type,
655 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
656 'company_id': order.company_id.id,
658 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
659 wf_service = netsvc.LocalService("workflow")
660 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
663 # No procurement because no product in the sale.order.line.
669 wf_service = netsvc.LocalService("workflow")
670 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
672 if order.state == 'shipping_except':
673 val['state'] = 'progress'
675 if (order.order_policy == 'manual'):
676 for line in order.order_line:
677 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
678 val['state'] = 'manual'
680 self.write(cr, uid, [order.id], val)
684 def action_ship_end(self, cr, uid, ids, context={}):
685 for order in self.browse(cr, uid, ids):
686 val = {'shipped': True}
687 if order.state == 'shipping_except':
688 val['state'] = 'progress'
689 if (order.order_policy == 'manual'):
690 for line in order.order_line:
691 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
692 val['state'] = 'manual'
694 for line in order.order_line:
696 if line.state == 'exception':
697 towrite.append(line.id)
699 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
700 self.write(cr, uid, [order.id], val)
703 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
704 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
706 part = inv['partner_id'] and inv['partner_id'][0]
707 pr = inv['amount_untaxed'] or 0.0
708 partnertype = 'customer'
711 'name': 'Order: '+name,
713 'description': 'Order '+str(inv['id']),
716 'date': time.strftime('%Y-%m-%d'),
719 'partner_type': partnertype,
721 'planned_revenue': pr,
725 self.pool.get('res.partner.event').create(cr, uid, event)
727 def has_stockable_products(self, cr, uid, ids, *args):
728 for order in self.browse(cr, uid, ids):
729 for order_line in order.order_line:
730 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
735 # TODO add a field price_unit_uos
736 # - update it on change product and unit price
737 # - use it in report if there is a uos
738 class sale_order_line(osv.osv):
739 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
741 for line in self.browse(cr, uid, ids):
742 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
745 def _amount_line(self, cr, uid, ids, field_name, arg, context):
747 cur_obj = self.pool.get('res.currency')
748 for line in self.browse(cr, uid, ids):
749 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
750 cur = line.order_id.pricelist_id.currency_id
751 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
754 def _number_packages(self, cr, uid, ids, field_name, arg, context):
756 for line in self.browse(cr, uid, ids):
758 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
763 _name = 'sale.order.line'
764 _description = 'Sale Order line'
766 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
767 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
768 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
769 '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)]}),
770 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
771 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
772 'invoiced': fields.boolean('Invoiced', readonly=True),
773 'procurement_id': fields.many2one('mrp.procurement', 'Requisition'),
774 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy'])), readonly=True, states={'draft':[('readonly',False)]}),
775 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
776 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
777 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
778 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Requisition Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
779 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
780 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
781 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
782 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}),
783 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
784 'product_uos': fields.many2one('product.uom', 'Product UoS'),
785 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
786 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
787 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
788 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
789 'notes': fields.text('Notes', translate=True),
790 'th_weight': fields.float('Weight'),
791 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
792 help=' * The \'Draft\' state is set automatically when sale order in draft state. \
793 \n* The \'Confirmed\' state is set automatically when sale order in confirm state. \
794 \n* The \'Exception\' state is set automatically when sale order is set as exception. \
795 \n* The \'Done\' state is set automatically when sale order is set as done. \
796 \n* The \'Cancelled\' state is set automatically when user cancel sale order.'),
797 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
798 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
799 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
801 _order = 'sequence, id'
803 'discount': lambda *a: 0.0,
804 'delay': lambda *a: 0.0,
805 'product_uom_qty': lambda *a: 1,
806 'product_uos_qty': lambda *a: 1,
807 'sequence': lambda *a: 10,
808 'invoiced': lambda *a: 0,
809 'state': lambda *a: 'draft',
810 'type': lambda *a: 'make_to_stock',
811 'product_packaging': lambda *a: False
814 def invoice_line_create(self, cr, uid, ids, context={}):
815 def _get_line_qty(line):
816 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
818 return line.product_uos_qty or 0.0
819 return line.product_uom_qty
821 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
822 line.procurement_id.id, context)
824 def _get_line_uom(line):
825 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
827 return line.product_uos.id
828 return line.product_uom.id
830 return self.pool.get('mrp.procurement').uom_get(cr, uid,
831 line.procurement_id.id, context)
835 for line in self.browse(cr, uid, ids, context):
836 if not line.invoiced:
838 a = line.product_id.product_tmpl_id.property_account_income.id
840 a = line.product_id.categ_id.property_account_income_categ.id
842 raise osv.except_osv(_('Error !'),
843 _('There is no income account defined ' \
844 'for this product: "%s" (id:%d)') % \
845 (line.product_id.name, line.product_id.id,))
847 a = self.pool.get('ir.property').get(cr, uid,
848 'property_account_income_categ', 'product.category',
850 uosqty = _get_line_qty(line)
851 uos_id = _get_line_uom(line)
854 pu = round(line.price_unit * line.product_uom_qty / uosqty,
855 int(config['price_accuracy']))
856 fpos = line.order_id.fiscal_position or False
857 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
859 raise osv.except_osv(_('Error !'),
860 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
861 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
863 'origin': line.order_id.name,
867 'discount': line.discount,
869 'product_id': line.product_id.id or False,
870 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
872 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
874 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
875 self.write(cr, uid, [line.id], {'invoiced': True})
877 sales[line.order_id.id] = True
878 create_ids.append(inv_id)
880 # Trigger workflow events
881 wf_service = netsvc.LocalService("workflow")
882 for sid in sales.keys():
883 wf_service.trg_write(uid, 'sale.order', sid, cr)
886 def button_cancel(self, cr, uid, ids, context={}):
887 for line in self.browse(cr, uid, ids, context=context):
889 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
890 return self.write(cr, uid, ids, {'state': 'cancel'})
892 def button_confirm(self, cr, uid, ids, context={}):
893 return self.write(cr, uid, ids, {'state': 'confirmed'})
895 def button_done(self, cr, uid, ids, context={}):
896 wf_service = netsvc.LocalService("workflow")
897 res = self.write(cr, uid, ids, {'state': 'done'})
898 for line in self.browse(cr, uid, ids, context):
899 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
903 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
904 product_obj = self.pool.get('product.product')
906 return {'value': {'product_uom': product_uos,
907 'product_uom_qty': product_uos_qty}, 'domain': {}}
909 product = product_obj.browse(cr, uid, product_id)
911 'product_uom': product.uom_id.id,
913 # FIXME must depend on uos/uom of the product and not only of the coeff.
916 'product_uom_qty': product_uos_qty / product.uos_coeff,
917 'th_weight': product_uos_qty / product.uos_coeff * product.weight
919 except ZeroDivisionError:
921 return {'value': value}
923 def copy_data(self, cr, uid, id, default=None, context={}):
926 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
927 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
929 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
930 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
931 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
933 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.'))
935 product_uom_obj = self.pool.get('product.uom')
936 partner_obj = self.pool.get('res.partner')
937 product_obj = self.pool.get('product.product')
939 lang = partner_obj.browse(cr, uid, partner_id).lang
940 context = {'lang': lang, 'partner_id': partner_id}
943 return {'value': {'th_weight': 0, 'product_packaging': False,
944 'product_uos_qty': qty}, 'domain': {'product_uom': [],
948 date_order = time.strftime('%Y-%m-%d')
951 product_obj = product_obj.browse(cr, uid, product, context=context)
952 if not packaging and product_obj.packaging:
953 packaging = product_obj.packaging[0].id
954 result['product_packaging'] = packaging
957 default_uom = product_obj.uom_id and product_obj.uom_id.id
958 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
959 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
960 # qty = qty - qty % q + q
961 if qty and (q and not (qty % q) == 0):
965 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)
966 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
968 'title': _('Picking Information !'),
971 result['product_uom_qty'] = qty
974 uom2 = product_uom_obj.browse(cr, uid, uom)
975 if product_obj.uom_id.category_id.id != uom2.category_id.id:
979 if product_obj.uos_id:
980 uos2 = product_uom_obj.browse(cr, uid, uos)
981 if product_obj.uos_id.category_id.id != uos2.category_id.id:
985 result.update({'type': product_obj.procure_method})
986 if product_obj.description_sale:
987 result['notes'] = product_obj.description_sale
988 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
989 if update_tax: #The quantity only have changed
990 result['delay'] = (product_obj.sale_delay or 0.0)
991 partner = partner_obj.browse(cr, uid, partner_id)
992 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
994 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
996 if (not uom) and (not uos):
997 result['product_uom'] = product_obj.uom_id.id
998 if product_obj.uos_id:
999 result['product_uos'] = product_obj.uos_id.id
1000 result['product_uos_qty'] = qty * product_obj.uos_coeff
1001 uos_category_id = product_obj.uos_id.category_id.id
1003 result['product_uos'] = False
1004 result['product_uos_qty'] = qty
1005 uos_category_id = False
1006 result['th_weight'] = qty * product_obj.weight
1007 domain = {'product_uom':
1008 [('category_id', '=', product_obj.uom_id.category_id.id)],
1010 [('category_id', '=', uos_category_id)]}
1012 elif uos: # only happens if uom is False
1013 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1014 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1015 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1016 elif uom: # whether uos is set or not
1017 default_uom = product_obj.uom_id and product_obj.uom_id.id
1018 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1019 if product_obj.uos_id:
1020 result['product_uos'] = product_obj.uos_id.id
1021 result['product_uos_qty'] = qty * product_obj.uos_coeff
1023 result['product_uos'] = False
1024 result['product_uos_qty'] = qty
1025 result['th_weight'] = q * product_obj.weight # Round the quantity up
1031 'title': 'No Pricelist !',
1033 'You have to select a pricelist in the sale form !\n'
1034 'Please set one before choosing a product.'
1037 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1038 product, qty or 1.0, partner_id, {
1044 'title': 'No valid pricelist line found !',
1046 "Couldn't find a pricelist line matching this product and quantity.\n"
1047 "You have to change either the product, the quantity or the pricelist."
1050 result.update({'price_unit': price})
1051 return {'value': result, 'domain': domain, 'warning': warning}
1053 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1054 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1055 lang=False, update_tax=True, date_order=False):
1056 res = self.product_id_change(cursor, user, ids, pricelist, product,
1057 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1058 partner_id=partner_id, lang=lang, update_tax=update_tax,
1059 date_order=date_order)
1060 if 'product_uom' in res['value']:
1061 del res['value']['product_uom']
1063 res['value']['price_unit'] = 0.0
1066 def unlink(self, cr, uid, ids, context={}):
1067 """Allows to delete sale order lines in draft,cancel states"""
1068 for rec in self.browse(cr, uid, ids, context=context):
1069 if rec.state not in ['draft', 'cancel']:
1070 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1071 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1076 class sale_config_picking_policy(osv.osv_memory):
1077 _name = 'sale.config.picking_policy'
1078 _inherit = 'res.config'
1081 'name': fields.char('Name', size=64),
1082 'picking_policy': fields.selection([
1083 ('direct', 'Direct Delivery'),
1084 ('one', 'All at Once')
1085 ], 'Picking Default Policy', required=True),
1086 'order_policy': fields.selection([
1087 ('manual', 'Invoice Based on Sales Orders'),
1088 ('picking', 'Invoice Based on Deliveries'),
1089 ], 'Shipping Default Policy', required=True),
1090 'step': fields.selection([
1091 ('one', 'Delivery Order Only'),
1092 ('two', 'Picking List & Delivery Order')
1093 ], 'Steps To Deliver a Sale Order', required=True,
1094 help="By default, Open ERP is able to manage complex routing and paths "\
1095 "of products in your warehouse and partner locations. This will configure "\
1096 "the most common and simple methods to deliver products to the customer "\
1097 "in one or two operations by the worker.")
1100 'picking_policy': lambda *a: 'direct',
1101 'order_policy': lambda *a: 'picking',
1102 'step': lambda *a: 'one'
1105 def execute(self, cr, uid, ids, context=None):
1106 for o in self.browse(cr, uid, ids, context=context):
1107 ir_values_obj = self.pool.get('ir.values')
1108 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1109 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1112 md = self.pool.get('ir.model.data')
1113 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1114 group_id = md.browse(cr, uid, group_id, context).res_id
1115 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1116 menu_id = md.browse(cr, uid, menu_id, context).res_id
1117 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1119 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1120 location_id = md.browse(cr, uid, location_id, context).res_id
1121 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1122 sale_config_picking_policy()