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 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41 'company_id': fields.many2one('res.company', 'Company', required=False),
44 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
49 class sale_order(osv.osv):
51 _inherit = ['ir.needaction_mixin', 'mail.thread']
52 _description = "Sales Order"
55 def copy(self, cr, uid, id, default=None, context=None):
63 'date_confirm': False,
64 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
66 return super(sale_order, self).copy(cr, uid, id, default, context=context)
68 def _amount_line_tax(self, cr, uid, line, context=None):
70 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.product_id, line.order_id.partner_id)['taxes']:
71 val += c.get('amount', 0.0)
74 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75 cur_obj = self.pool.get('res.currency')
77 for order in self.browse(cr, uid, ids, context=context):
79 'amount_untaxed': 0.0,
84 cur = order.pricelist_id.currency_id
85 for line in order.order_line:
86 val1 += line.price_subtotal
87 val += self._amount_line_tax(cr, uid, line, context=context)
88 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
94 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
100 tmp[id] = {'picked': 0.0, 'total': 0.0}
102 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
106 stock_picking p on (p.id=m.picking_id)
108 procurement_order mp on (mp.move_id=m.id)
110 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
112 for item in cr.dictfetchall():
113 if item['move_state'] == 'cancel':
116 if item['picking_type'] == 'in':#this is a returned picking
117 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
121 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
125 for order in self.browse(cr, uid, ids, context=context):
127 res[order.id] = 100.0
129 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
132 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
134 for sale in self.browse(cursor, user, ids, context=context):
139 for invoice in sale.invoice_ids:
140 if invoice.state not in ('draft', 'cancel'):
141 tot += invoice.amount_untaxed
143 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
148 def _invoiced(self, cursor, user, ids, name, arg, context=None):
150 for sale in self.browse(cursor, user, ids, context=context):
152 invoice_existence = False
153 for invoice in sale.invoice_ids:
154 if invoice.state!='cancel':
155 invoice_existence = True
156 if invoice.state != 'paid':
159 if not invoice_existence:
163 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
172 clause += 'AND inv.state = \'paid\''
174 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
175 sale_clause = ', sale_order AS sale '
178 cursor.execute('SELECT rel.order_id ' \
179 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180 'WHERE rel.invoice_id = inv.id ' + clause)
181 res = cursor.fetchall()
183 cursor.execute('SELECT sale.id ' \
184 'FROM sale_order AS sale ' \
185 'WHERE sale.id NOT IN ' \
186 '(SELECT rel.order_id ' \
187 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188 res.extend(cursor.fetchall())
190 return [('id', '=', 0)]
191 return [('id', 'in', [x[0] for x in res])]
193 def _get_order(self, cr, uid, ids, context=None):
195 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196 result[line.order_id.id] = True
200 'name': fields.char('Order Reference', size=64, required=True,
201 readonly=True, states={'draft': [('readonly', False)]}, select=True),
202 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
203 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204 'client_order_ref': fields.char('Customer Reference', size=64),
205 'state': fields.selection([
206 ('draft', 'Quotation'),
207 ('cancel', 'Cancelled'),
208 ('waiting_date', 'Waiting Schedule'),
209 ('manual', 'To Invoice'),
210 ('progress', 'In Progress'),
211 ('shipping_except', 'Shipping Exception'),
212 ('invoice_except', 'Invoice Exception'),
214 ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
215 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
216 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
217 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
218 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
219 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
220 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
221 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
223 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
224 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
226 help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
227 'order_policy': fields.selection([
228 ('manual', 'On Demand'),
229 ('picking', 'On Delivery Order'),
230 ('prepaid', 'Before Delivery'),
231 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)]},
232 help="""This field controls how invoice and delivery operations are synchronized.
233 - With 'On Demand', the invoice is created manually when needed.
234 - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
235 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
236 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
237 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
239 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
240 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
241 'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
242 'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
243 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
244 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
245 'invoiced': fields.function(_invoiced, string='Paid',
246 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
247 'note': fields.text('Notes'),
249 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
251 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
252 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254 multi='sums', help="The amount without tax."),
255 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
257 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
258 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
260 multi='sums', help="The tax amount."),
261 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
263 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
264 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
266 multi='sums', help="The total amount."),
268 '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 want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
269 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
270 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
271 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
274 'picking_policy': 'direct',
275 'date_order': fields.date.context_today,
276 'order_policy': 'manual',
278 'user_id': lambda obj, cr, uid, context: uid,
279 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
280 'invoice_quantity': 'order',
281 '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'],
282 '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'],
285 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
290 def unlink(self, cr, uid, ids, context=None):
291 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
293 for s in sale_orders:
294 if s['state'] in ['draft', 'cancel']:
295 unlink_ids.append(s['id'])
297 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
299 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
301 def onchange_shop_id(self, cr, uid, ids, shop_id):
304 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
305 v['project_id'] = shop.project_id.id
306 # Que faire si le client a une pricelist a lui ?
307 if shop.pricelist_id.id:
308 v['pricelist_id'] = shop.pricelist_id.id
311 def action_cancel_draft(self, cr, uid, ids, context=None):
314 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
315 line_ids = map(lambda x: x[0], cr.fetchall())
316 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
317 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
318 wf_service = netsvc.LocalService("workflow")
320 # Deleting the existing instance of workflow for SO
321 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
322 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
323 self.action_cancel_draft_send_note(cr, uid, ids, context=context)
326 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327 if (not pricelist_id) or (not order_lines):
330 'title': _('Pricelist Warning!'),
331 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333 return {'warning': warning}
335 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
340 val['partner_invoice_id'] = order_id
342 val['partner_shipping_id'] = order_id
343 return {'value': val}
345 def onchange_partner_id(self, cr, uid, ids, part):
347 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
349 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350 part = self.pool.get('res.partner').browse(cr, uid, part)
351 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352 payment_term = part.property_payment_term and part.property_payment_term.id or False
353 fiscal_position = part.property_account_position and part.property_account_position.id or False
354 dedicated_salesman = part.user_id and part.user_id.id or uid
356 'partner_invoice_id': addr['invoice'],
357 'partner_shipping_id': addr['delivery'],
358 'payment_term': payment_term,
359 'fiscal_position': fiscal_position,
360 'user_id': dedicated_salesman,
363 val['pricelist_id'] = pricelist
364 return {'value': val}
366 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
370 if policy == 'prepaid':
372 elif policy == 'picking':
373 inv_qty = 'procurement'
374 return {'value': {'invoice_quantity': inv_qty}}
376 def write(self, cr, uid, ids, vals, context=None):
377 if vals.get('order_policy', False):
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):
385 if vals.get('order_policy', False):
386 if vals['order_policy'] == 'prepaid':
387 vals.update({'invoice_quantity': 'order'})
388 if vals['order_policy'] == 'picking':
389 vals.update({'invoice_quantity': 'procurement'})
390 order = super(sale_order, self).create(cr, uid, vals, context=context)
392 self.create_send_note(cr, uid, [order], context=context)
395 def button_dummy(self, cr, uid, ids, context=None):
398 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
399 # can be removed after 6.1.
400 def _inv_get(self, cr, uid, order, context=None):
403 def _prepare_invoice(self, cr, uid, order, lines, context=None):
404 """Prepare the dict of values to create the new invoice for a
405 sale order. This method may be overridden to implement custom
406 invoice generation (making sure to call super() to establish
407 a clean extension chain).
409 :param browse_record order: sale.order record to invoice
410 :param list(int) line: list of invoice line IDs that must be
411 attached to the invoice
412 :return: dict of value to create() the invoice
416 journal_ids = self.pool.get('account.journal').search(cr, uid,
417 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
420 raise osv.except_osv(_('Error !'),
421 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
424 'name': order.client_order_ref or '',
425 'origin': order.name,
426 'type': 'out_invoice',
427 'reference': order.client_order_ref or order.name,
428 'account_id': order.partner_id.property_account_receivable.id,
429 'partner_id': order.partner_id.id,
430 'journal_id': journal_ids[0],
431 'invoice_line': [(6, 0, lines)],
432 'currency_id': order.pricelist_id.currency_id.id,
433 'comment': order.note,
434 'payment_term': order.payment_term and order.payment_term.id or False,
435 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
436 'date_invoice': context.get('date_invoice', False),
437 'company_id': order.company_id.id,
438 'user_id': order.user_id and order.user_id.id or False
441 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
442 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
446 def _make_invoice(self, cr, uid, order, lines, context=None):
447 inv_obj = self.pool.get('account.invoice')
448 obj_invoice_line = self.pool.get('account.invoice.line')
451 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
452 from_line_invoice_ids = []
453 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
454 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
455 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
456 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
457 for preinv in order.invoice_ids:
458 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
459 for preline in preinv.invoice_line:
460 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
461 lines.append(inv_line_id)
462 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
463 inv_id = inv_obj.create(cr, uid, inv, context=context)
464 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
465 if data.get('value', False):
466 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
467 inv_obj.button_compute(cr, uid, [inv_id])
470 def manual_invoice(self, cr, uid, ids, context=None):
471 mod_obj = self.pool.get('ir.model.data')
472 wf_service = netsvc.LocalService("workflow")
476 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
477 inv_ids.add(record.id)
478 # inv_ids would have old invoices if any
480 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
481 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
482 inv_ids1.add(record.id)
483 inv_ids = list(inv_ids1.difference(inv_ids))
485 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
486 res_id = res and res[1] or False,
489 'name': _('Customer Invoices'),
493 'res_model': 'account.invoice',
494 'context': "{'type':'out_invoice'}",
495 'type': 'ir.actions.act_window',
498 'res_id': inv_ids and inv_ids[0] or False,
501 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
505 picking_obj = self.pool.get('stock.picking')
506 invoice = self.pool.get('account.invoice')
507 obj_sale_order_line = self.pool.get('sale.order.line')
508 partner_currency = {}
511 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
512 # last day of the last month as invoice date
514 context['date_inv'] = date_inv
515 for o in self.browse(cr, uid, ids, context=context):
516 currency_id = o.pricelist_id.currency_id.id
517 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
518 raise osv.except_osv(
520 _('You cannot group sales having different currencies for the same partner.'))
522 partner_currency[o.partner_id.id] = currency_id
524 for line in o.order_line:
527 elif (line.state in states):
528 lines.append(line.id)
529 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
531 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
533 for o in self.browse(cr, uid, ids, context=context):
534 for i in o.invoice_ids:
535 if i.state == 'draft':
537 for val in invoices.values():
539 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
542 invoice_ref += o.name + '|'
543 self.write(cr, uid, [o.id], {'state': 'progress'})
544 if o.order_policy == 'picking':
545 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
546 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
547 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
549 for order, il in val:
550 res = self._make_invoice(cr, uid, order, il, context=context)
551 invoice_ids.append(res)
552 self.write(cr, uid, [order.id], {'state': 'progress'})
553 if order.order_policy == 'picking':
554 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
555 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
557 self.invoice_send_note(cr, uid, ids, res, context)
560 def action_invoice_cancel(self, cr, uid, ids, context=None):
563 for sale in self.browse(cr, uid, ids, context=context):
564 for line in sale.order_line:
566 # Check if the line is invoiced (has asociated invoice
567 # lines from non-cancelled invoices).
570 for iline in line.invoice_lines:
571 if iline.invoice_id and iline.invoice_id.state != 'cancel':
574 # Update the line (only when needed)
575 if line.invoiced != invoiced:
576 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
577 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
580 def action_invoice_end(self, cr, uid, ids, context=None):
581 for order in self.browse(cr, uid, ids, context=context):
583 # Update the sale order lines state (and invoiced flag).
585 for line in order.order_line:
588 # Check if the line is invoiced (has asociated invoice
589 # lines from non-cancelled invoices).
592 for iline in line.invoice_lines:
593 if iline.invoice_id and iline.invoice_id.state != 'cancel':
596 if line.invoiced != invoiced:
597 vals['invoiced'] = invoiced
598 # If the line was in exception state, now it gets confirmed.
599 if line.state == 'exception':
600 vals['state'] = 'confirmed'
601 # Update the line (only when needed).
603 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
605 # Update the sales order state.
607 if order.state == 'invoice_except':
608 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
611 def action_cancel(self, cr, uid, ids, context=None):
612 wf_service = netsvc.LocalService("workflow")
615 sale_order_line_obj = self.pool.get('sale.order.line')
616 proc_obj = self.pool.get('procurement.order')
617 for sale in self.browse(cr, uid, ids, context=context):
618 for pick in sale.picking_ids:
619 if pick.state not in ('draft', 'cancel'):
620 raise osv.except_osv(
621 _('Could not cancel sales order !'),
622 _('You must first cancel all picking attached to this sales order.'))
623 if pick.state == 'cancel':
624 for mov in pick.move_lines:
625 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
627 for proc in proc_ids:
628 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
629 for r in self.read(cr, uid, ids, ['picking_ids']):
630 for pick in r['picking_ids']:
631 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
632 for inv in sale.invoice_ids:
633 if inv.state not in ('draft', 'cancel'):
634 raise osv.except_osv(
635 _('Could not cancel this sales order !'),
636 _('You must first cancel all invoices attached to this sales order.'))
637 for r in self.read(cr, uid, ids, ['invoice_ids']):
638 for inv in r['invoice_ids']:
639 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
640 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
642 self.cancel_send_note(cr, uid, [sale.id], context=None)
643 self.write(cr, uid, ids, {'state': 'cancel'})
646 def action_wait(self, cr, uid, ids, context=None):
647 for o in self.browse(cr, uid, ids):
649 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
650 if (o.order_policy == 'manual'):
651 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
653 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
654 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
655 self.confirm_send_note(cr, uid, ids, context)
658 def procurement_lines_get(self, cr, uid, ids, *args):
660 for order in self.browse(cr, uid, ids, context={}):
661 for line in order.order_line:
662 if line.procurement_id:
663 res.append(line.procurement_id.id)
666 # if mode == 'finished':
667 # returns True if all lines are done, False otherwise
668 # if mode == 'canceled':
669 # returns True if there is at least one canceled line, False otherwise
670 def test_state(self, cr, uid, ids, mode, *args):
671 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
676 write_cancel_ids = []
677 for order in self.browse(cr, uid, ids, context={}):
678 for line in order.order_line:
679 if (not line.procurement_id) or (line.procurement_id.state=='done'):
680 if line.state != 'done':
681 write_done_ids.append(line.id)
684 if line.procurement_id:
685 if (line.procurement_id.state == 'cancel'):
687 if line.state != 'exception':
688 write_cancel_ids.append(line.id)
692 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
694 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
696 if mode == 'finished':
698 elif mode == 'canceled':
704 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
707 'origin': order.name,
708 'date_planned': date_planned,
709 'product_id': line.product_id.id,
710 'product_qty': line.product_uom_qty,
711 'product_uom': line.product_uom.id,
712 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
713 or line.product_uom_qty,
714 'product_uos': (line.product_uos and line.product_uos.id)\
715 or line.product_uom.id,
716 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
717 'procure_method': line.type,
719 'company_id': order.company_id.id,
723 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
724 location_id = order.shop_id.warehouse_id.lot_stock_id.id
725 output_id = order.shop_id.warehouse_id.lot_output_id.id
727 'name': line.name[:250],
728 'picking_id': picking_id,
729 'product_id': line.product_id.id,
730 'date': date_planned,
731 'date_expected': date_planned,
732 'product_qty': line.product_uom_qty,
733 'product_uom': line.product_uom.id,
734 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
735 'product_uos': (line.product_uos and line.product_uos.id)\
736 or line.product_uom.id,
737 'product_packaging': line.product_packaging.id,
738 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
739 'location_id': location_id,
740 'location_dest_id': output_id,
741 'sale_line_id': line.id,
742 'tracking_id': False,
746 'company_id': order.company_id.id,
747 'price_unit': line.product_id.standard_price or 0.0
750 def _prepare_order_picking(self, cr, uid, order, context=None):
751 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
754 'origin': order.name,
755 'date': order.date_order,
758 'move_type': order.picking_policy,
760 'partner_id': order.partner_shipping_id.id,
762 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
763 'company_id': order.company_id.id,
766 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
767 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
769 Define ship_recreate for process after shipping exception
770 param order: sale order to which the order lines belong
771 param line: sale order line records to procure
772 param move_id: the ID of stock move
773 param proc_id: the ID of procurement
775 move_obj = self.pool.get('stock.move')
776 if order.state == 'shipping_except':
777 for pick in order.picking_ids:
778 for move in pick.move_lines:
779 if move.state == 'cancel':
780 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
782 for mov in move_obj.browse(cr, uid, mov_ids):
783 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
784 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
785 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
788 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
789 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
790 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
793 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
794 """Create the required procurements to supply sale order lines, also connecting
795 the procurements to appropriate stock moves in order to bring the goods to the
796 sale order's requested location.
798 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
799 a standard outgoing picking will be created to wrap the stock moves, as returned
800 by :meth:`~._prepare_order_picking`.
802 Modules that wish to customize the procurements or partition the stock moves over
803 multiple stock pickings may override this method and call ``super()`` with
804 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
806 :param browse_record order: sale order to which the order lines belong
807 :param list(browse_record) order_lines: sale order line records to procure
808 :param int picking_id: optional ID of a stock picking to which the created stock moves
809 will be added. A new picking will be created if ommitted.
812 move_obj = self.pool.get('stock.move')
813 picking_obj = self.pool.get('stock.picking')
814 procurement_obj = self.pool.get('procurement.order')
817 for line in order_lines:
818 if line.state == 'done':
821 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
824 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
826 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
827 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
829 # a service has no stock move
832 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
833 proc_ids.append(proc_id)
834 line.write({'procurement_id': proc_id})
835 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
837 wf_service = netsvc.LocalService("workflow")
839 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
840 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
843 for proc_id in proc_ids:
844 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
847 if order.state == 'shipping_except':
848 val['state'] = 'progress'
849 val['shipped'] = False
851 if (order.order_policy == 'manual'):
852 for line in order.order_line:
853 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
854 val['state'] = 'manual'
859 def action_ship_create(self, cr, uid, ids, context=None):
860 for order in self.browse(cr, uid, ids, context=context):
861 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
864 def action_ship_end(self, cr, uid, ids, context=None):
865 for order in self.browse(cr, uid, ids, context=context):
866 val = {'shipped': True}
867 if order.state == 'shipping_except':
868 val['state'] = 'progress'
869 if (order.order_policy == 'manual'):
870 for line in order.order_line:
871 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
872 val['state'] = 'manual'
874 for line in order.order_line:
876 if line.state == 'exception':
877 towrite.append(line.id)
879 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
880 res = self.write(cr, uid, [order.id], val)
882 self.delivery_end_send_note(cr, uid, [order.id], context=context)
885 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
886 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
888 part = inv['partner_id'] and inv['partner_id'][0]
889 pr = inv['amount_untaxed'] or 0.0
890 partnertype = 'customer'
893 'name': 'Order: '+name,
895 'description': 'Order '+str(inv['id']),
898 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
900 'partner_type': partnertype,
902 'planned_revenue': pr,
906 self.pool.get('res.partner.event').create(cr, uid, event)
908 def has_stockable_products(self, cr, uid, ids, *args):
909 for order in self.browse(cr, uid, ids):
910 for order_line in order.order_line:
911 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
915 # ------------------------------------------------
916 # OpenChatter methods and notifications
917 # ------------------------------------------------
919 def get_needaction_user_ids(self, cr, uid, ids, context=None):
920 result = dict.fromkeys(ids, [])
921 for obj in self.browse(cr, uid, ids, context=context):
922 if (obj.state == 'manual' or obj.state == 'progress'):
923 result[obj.id] = [obj.user_id.id]
926 def create_send_note(self, cr, uid, ids, context=None):
927 for obj in self.browse(cr, uid, ids, context=context):
928 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
929 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
931 def confirm_send_note(self, cr, uid, ids, context=None):
932 for obj in self.browse(cr, uid, ids, context=context):
933 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
935 def cancel_send_note(self, cr, uid, ids, context=None):
936 for obj in self.browse(cr, uid, ids, context=context):
937 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
939 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
940 for order in self.browse(cr, uid, ids, context=context):
941 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
942 # convert datetime field to a datetime, using server format, then
943 # convert it to the user TZ and re-render it with %Z to add the timezone
944 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
945 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
946 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
948 def delivery_end_send_note(self, cr, uid, ids, context=None):
949 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
951 def invoice_paid_send_note(self, cr, uid, ids, context=None):
952 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
954 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
955 for order in self.browse(cr, uid, ids, context=context):
956 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
957 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
959 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
960 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
965 # TODO add a field price_unit_uos
966 # - update it on change product and unit price
967 # - use it in report if there is a uos
968 class sale_order_line(osv.osv):
970 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
971 tax_obj = self.pool.get('account.tax')
972 cur_obj = self.pool.get('res.currency')
976 for line in self.browse(cr, uid, ids, context=context):
977 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
978 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
979 cur = line.order_id.pricelist_id.currency_id
980 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
983 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
985 for line in self.browse(cr, uid, ids, context=context):
987 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
992 def _get_uom_id(self, cr, uid, *args):
994 proxy = self.pool.get('ir.model.data')
995 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
997 except Exception, ex:
1000 _name = 'sale.order.line'
1001 _description = 'Sales Order Line'
1003 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1004 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1005 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1006 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
1007 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1008 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1009 'invoiced': fields.boolean('Invoiced', readonly=True),
1010 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1011 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1012 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1013 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1014 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1015 help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
1016 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1017 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1018 'product_uom_qty': fields.float('Quantity (Unit of Measure)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1019 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1020 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1021 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1022 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1023 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1024 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1025 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1026 'notes': fields.text('Notes'),
1027 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1028 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
1029 help='* The \'Draft\' state is set when the related sales order in draft state. \
1030 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1031 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1032 \n* The \'Done\' state is set when the sales order line has been picked. \
1033 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1034 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1035 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
1036 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1038 _order = 'sequence, id'
1040 'product_uom' : _get_uom_id,
1043 'product_uom_qty': 1,
1044 'product_uos_qty': 1,
1048 'type': 'make_to_stock',
1049 'product_packaging': False,
1053 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1054 """Prepare the dict of values to create the new invoice line for a
1055 sale order line. This method may be overridden to implement custom
1056 invoice generation (making sure to call super() to establish
1057 a clean extension chain).
1059 :param browse_record line: sale.order.line record to invoice
1060 :param int account_id: optional ID of a G/L account to force
1061 (this is used for returning products including service)
1062 :return: dict of values to create() the invoice line
1065 def _get_line_qty(line):
1066 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1067 if line.product_uos:
1068 return line.product_uos_qty or 0.0
1069 return line.product_uom_qty
1071 return self.pool.get('procurement.order').quantity_get(cr, uid,
1072 line.procurement_id.id, context=context)
1074 def _get_line_uom(line):
1075 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1076 if line.product_uos:
1077 return line.product_uos.id
1078 return line.product_uom.id
1080 return self.pool.get('procurement.order').uom_get(cr, uid,
1081 line.procurement_id.id, context=context)
1083 if not line.invoiced:
1086 account_id = line.product_id.product_tmpl_id.property_account_income.id
1088 account_id = line.product_id.categ_id.property_account_income_categ.id
1090 raise osv.except_osv(_('Error !'),
1091 _('There is no income account defined for this product: "%s" (id:%d)') % \
1092 (line.product_id.name, line.product_id.id,))
1094 prop = self.pool.get('ir.property').get(cr, uid,
1095 'property_account_income_categ', 'product.category',
1097 account_id = prop and prop.id or False
1098 uosqty = _get_line_qty(line)
1099 uos_id = _get_line_uom(line)
1102 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1103 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1104 fpos = line.order_id.fiscal_position or False
1105 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1107 raise osv.except_osv(_('Error !'),
1108 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1111 'origin': line.order_id.name,
1112 'account_id': account_id,
1115 'discount': line.discount,
1117 'product_id': line.product_id.id or False,
1118 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1120 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1125 def invoice_line_create(self, cr, uid, ids, context=None):
1131 for line in self.browse(cr, uid, ids, context=context):
1132 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1134 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1135 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1136 self.write(cr, uid, [line.id], {'invoiced': True})
1137 sales.add(line.order_id.id)
1138 create_ids.append(inv_id)
1139 # Trigger workflow events
1140 wf_service = netsvc.LocalService("workflow")
1141 for sale_id in sales:
1142 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1145 def button_cancel(self, cr, uid, ids, context=None):
1146 for line in self.browse(cr, uid, ids, context=context):
1148 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1149 for move_line in line.move_ids:
1150 if move_line.state != 'cancel':
1151 raise osv.except_osv(
1152 _('Could not cancel sales order line!'),
1153 _('You must first cancel stock moves attached to this sales order line.'))
1154 return self.write(cr, uid, ids, {'state': 'cancel'})
1156 def button_confirm(self, cr, uid, ids, context=None):
1157 return self.write(cr, uid, ids, {'state': 'confirmed'})
1159 def button_done(self, cr, uid, ids, context=None):
1160 wf_service = netsvc.LocalService("workflow")
1161 res = self.write(cr, uid, ids, {'state': 'done'})
1162 for line in self.browse(cr, uid, ids, context=context):
1163 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1166 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1167 product_obj = self.pool.get('product.product')
1169 return {'value': {'product_uom': product_uos,
1170 'product_uom_qty': product_uos_qty}, 'domain': {}}
1172 product = product_obj.browse(cr, uid, product_id)
1174 'product_uom': product.uom_id.id,
1176 # FIXME must depend on uos/uom of the product and not only of the coeff.
1179 'product_uom_qty': product_uos_qty / product.uos_coeff,
1180 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1182 except ZeroDivisionError:
1184 return {'value': value}
1186 def copy_data(self, cr, uid, id, default=None, context=None):
1189 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1190 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1192 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1193 partner_id=False, packaging=False, flag=False, context=None):
1195 return {'value': {'product_packaging': False}}
1196 product_obj = self.pool.get('product.product')
1197 product_uom_obj = self.pool.get('product.uom')
1198 pack_obj = self.pool.get('product.packaging')
1203 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1204 product=product, qty=qty, uom=uom, partner_id=partner_id,
1205 packaging=packaging, flag=False, context=context)
1206 warning_msgs = res.get('warning') and res['warning']['message']
1208 products = product_obj.browse(cr, uid, product, context=context)
1209 if not products.packaging:
1210 packaging = result['product_packaging'] = False
1211 elif not packaging and products.packaging and not flag:
1212 packaging = products.packaging[0].id
1213 result['product_packaging'] = packaging
1216 default_uom = products.uom_id and products.uom_id.id
1217 pack = pack_obj.browse(cr, uid, packaging, context=context)
1218 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1219 # qty = qty - qty % q + q
1220 if qty and (q and not (qty % q) == 0):
1221 ean = pack.ean or _('(n/a)')
1224 if not warning_msgs:
1225 warn_msg = _("You selected a quantity of %d Units.\n"
1226 "But it's not compatible with the selected packaging.\n"
1227 "Here is a proposition of quantities according to the packaging:\n"
1228 "EAN: %s Quantity: %s Type of ul: %s") % \
1229 (qty, ean, qty_pack, type_ul.name)
1230 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1232 'title': _('Configuration Error !'),
1233 'message': warning_msgs
1235 result['product_uom_qty'] = qty
1237 return {'value': result, 'warning': warning}
1239 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1240 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1241 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1242 context = context or {}
1243 lang = lang or context.get('lang',False)
1245 raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1247 product_uom_obj = self.pool.get('product.uom')
1248 partner_obj = self.pool.get('res.partner')
1249 product_obj = self.pool.get('product.product')
1250 context = {'lang': lang, 'partner_id': partner_id}
1252 lang = partner_obj.browse(cr, uid, partner_id).lang
1253 context_partner = {'lang': lang, 'partner_id': partner_id}
1256 return {'value': {'th_weight': 0, 'product_packaging': False,
1257 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1260 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1262 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1263 result = res.get('value', {})
1264 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1265 product_obj = product_obj.browse(cr, uid, product, context=context)
1269 uom2 = product_uom_obj.browse(cr, uid, uom)
1270 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1273 if product_obj.uos_id:
1274 uos2 = product_uom_obj.browse(cr, uid, uos)
1275 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1279 if product_obj.description_sale:
1280 result['notes'] = product_obj.description_sale
1281 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1282 if update_tax: #The quantity only have changed
1283 result['delay'] = (product_obj.sale_delay or 0.0)
1284 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1285 result.update({'type': product_obj.procure_method})
1288 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1290 if (not uom) and (not uos):
1291 result['product_uom'] = product_obj.uom_id.id
1292 if product_obj.uos_id:
1293 result['product_uos'] = product_obj.uos_id.id
1294 result['product_uos_qty'] = qty * product_obj.uos_coeff
1295 uos_category_id = product_obj.uos_id.category_id.id
1297 result['product_uos'] = False
1298 result['product_uos_qty'] = qty
1299 uos_category_id = False
1300 result['th_weight'] = qty * product_obj.weight
1301 domain = {'product_uom':
1302 [('category_id', '=', product_obj.uom_id.category_id.id)],
1304 [('category_id', '=', uos_category_id)]}
1306 elif uos and not uom: # only happens if uom is False
1307 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1308 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1309 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1310 elif uom: # whether uos is set or not
1311 default_uom = product_obj.uom_id and product_obj.uom_id.id
1312 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1313 if product_obj.uos_id:
1314 result['product_uos'] = product_obj.uos_id.id
1315 result['product_uos_qty'] = qty * product_obj.uos_coeff
1317 result['product_uos'] = False
1318 result['product_uos_qty'] = qty
1319 result['th_weight'] = q * product_obj.weight # Round the quantity up
1322 uom2 = product_obj.uom_id
1323 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1324 if (product_obj.type=='product') and int(compare_qty) == -1 \
1325 and (product_obj.procure_method=='make_to_stock'):
1326 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1327 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1328 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1329 max(0,product_obj.qty_available), product_obj.uom_id.name)
1330 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1334 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1335 'Please set one before choosing a product.')
1336 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1338 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1339 product, qty or 1.0, partner_id, {
1340 'uom': uom or result.get('product_uom'),
1344 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1345 "You have to change either the product, the quantity or the pricelist.")
1347 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1349 result.update({'price_unit': price})
1352 'title': _('Configuration Error !'),
1353 'message' : warning_msgs
1355 return {'value': result, 'domain': domain, 'warning': warning}
1357 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1358 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1359 lang=False, update_tax=True, date_order=False, context=None):
1360 context = context or {}
1361 lang = lang or ('lang' in context and context['lang'])
1362 res = self.product_id_change(cursor, user, ids, pricelist, product,
1363 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1364 partner_id=partner_id, lang=lang, update_tax=update_tax,
1365 date_order=date_order, context=context)
1366 if 'product_uom' in res['value']:
1367 del res['value']['product_uom']
1369 res['value']['price_unit'] = 0.0
1372 def unlink(self, cr, uid, ids, context=None):
1375 """Allows to delete sales order lines in draft,cancel states"""
1376 for rec in self.browse(cr, uid, ids, context=context):
1377 if rec.state not in ['draft', 'cancel']:
1378 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1379 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1383 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: