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)], 'sent': [('readonly', False)]}, select=True),
202 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('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', 'Draft Quotation'),
207 ('sent', 'Quotation Sent'),
208 ('cancel', 'Cancelled'),
209 ('waiting_date', 'Waiting Schedule'),
210 ('progress', 'Sale Order'),
211 ('manual', 'Sale to Invoice'),
212 ('shipping_except', 'Shipping Exception'),
213 ('invoice_except', 'Invoice Exception'),
215 ], 'Status', 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),
216 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
217 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
218 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
219 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
220 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
221 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
222 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."),
224 '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."),
225 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
226 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
227 help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
228 'order_policy': fields.selection([
229 ('manual', 'On Demand'),
230 ('picking', 'On Delivery Order'),
231 ('prepaid', 'Before Delivery'),
232 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
233 help="""This field controls how invoice and delivery operations are synchronized.
234 - With 'On Demand', the invoice is created manually when needed.
235 - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
236 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
238 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
240 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
241 '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)."),
242 '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."),
243 '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."),
244 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246 'invoiced': fields.function(_invoiced, string='Paid',
247 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248 'note': fields.text('Terms and conditions'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
255 multi='sums', help="The amount without tax."),
256 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
261 multi='sums', help="The tax amount."),
262 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
264 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
267 multi='sums', help="The total amount."),
269 '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)]}),
270 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
275 'picking_policy': 'direct',
276 'date_order': fields.date.context_today,
277 'order_policy': 'manual',
279 'user_id': lambda obj, cr, uid, context: uid,
280 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281 'invoice_quantity': 'order',
282 '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'],
283 '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'],
286 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
291 def unlink(self, cr, uid, ids, context=None):
292 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
294 for s in sale_orders:
295 if s['state'] in ['draft', 'cancel']:
296 unlink_ids.append(s['id'])
298 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sales order, you must cancel it before ! To cancel a sale order, you must first cancel related picking for delivery orders.'))
300 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
302 def onchange_shop_id(self, cr, uid, ids, shop_id):
305 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
306 v['project_id'] = shop.project_id.id
307 # Que faire si le client a une pricelist a lui ?
308 if shop.pricelist_id.id:
309 v['pricelist_id'] = shop.pricelist_id.id
312 def action_cancel_draft(self, cr, uid, ids, context=None):
315 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
316 line_ids = map(lambda x: x[0], cr.fetchall())
317 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
318 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
319 wf_service = netsvc.LocalService("workflow")
321 # Deleting the existing instance of workflow for SO
322 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
323 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
324 self.action_cancel_draft_send_note(cr, uid, ids, context=context)
327 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328 if (not pricelist_id) or (not order_lines):
331 'title': _('Pricelist Warning!'),
332 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
334 return {'warning': warning}
336 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
341 val['partner_invoice_id'] = order_id
343 val['partner_shipping_id'] = order_id
344 return {'value': val}
346 def onchange_partner_id(self, cr, uid, ids, part):
348 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
350 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
351 part = self.pool.get('res.partner').browse(cr, uid, part)
352 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
353 payment_term = part.property_payment_term and part.property_payment_term.id or False
354 fiscal_position = part.property_account_position and part.property_account_position.id or False
355 dedicated_salesman = part.user_id and part.user_id.id or uid
357 'partner_invoice_id': addr['invoice'],
358 'partner_shipping_id': addr['delivery'],
359 'payment_term': payment_term,
360 'fiscal_position': fiscal_position,
361 'user_id': dedicated_salesman,
364 val['pricelist_id'] = pricelist
365 return {'value': val}
367 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
371 if policy == 'prepaid':
373 elif policy == 'picking':
374 inv_qty = 'procurement'
375 return {'value': {'invoice_quantity': inv_qty}}
377 def write(self, cr, uid, ids, vals, context=None):
378 if vals.get('order_policy', False):
379 if vals['order_policy'] == 'prepaid':
380 vals.update({'invoice_quantity': 'order'})
381 elif vals['order_policy'] == 'picking':
382 vals.update({'invoice_quantity': 'procurement'})
383 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
385 def create(self, cr, uid, vals, context=None):
386 if vals.get('order_policy', False):
387 if vals['order_policy'] == 'prepaid':
388 vals.update({'invoice_quantity': 'order'})
389 if vals['order_policy'] == 'picking':
390 vals.update({'invoice_quantity': 'procurement'})
391 order = super(sale_order, self).create(cr, uid, vals, context=context)
393 self.create_send_note(cr, uid, [order], context=context)
396 def button_dummy(self, cr, uid, ids, context=None):
399 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
400 # can be removed after 6.1.
401 def _inv_get(self, cr, uid, order, context=None):
404 def _prepare_invoice(self, cr, uid, order, lines, context=None):
405 """Prepare the dict of values to create the new invoice for a
406 sale order. This method may be overridden to implement custom
407 invoice generation (making sure to call super() to establish
408 a clean extension chain).
410 :param browse_record order: sale.order record to invoice
411 :param list(int) line: list of invoice line IDs that must be
412 attached to the invoice
413 :return: dict of value to create() the invoice
417 journal_ids = self.pool.get('account.journal').search(cr, uid,
418 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
421 raise osv.except_osv(_('Error !'),
422 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
425 'name': order.client_order_ref or '',
426 'origin': order.name,
427 'type': 'out_invoice',
428 'reference': order.client_order_ref or order.name,
429 'account_id': order.partner_id.property_account_receivable.id,
430 'partner_id': order.partner_id.id,
431 'journal_id': journal_ids[0],
432 'invoice_line': [(6, 0, lines)],
433 'currency_id': order.pricelist_id.currency_id.id,
434 'comment': order.note,
435 'payment_term': order.payment_term and order.payment_term.id or False,
436 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
437 'date_invoice': context.get('date_invoice', False),
438 'company_id': order.company_id.id,
439 'user_id': order.user_id and order.user_id.id or False
442 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
443 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
447 def _make_invoice(self, cr, uid, order, lines, context=None):
448 inv_obj = self.pool.get('account.invoice')
449 obj_invoice_line = self.pool.get('account.invoice.line')
452 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
453 from_line_invoice_ids = []
454 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
455 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
456 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
457 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
458 for preinv in order.invoice_ids:
459 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
460 for preline in preinv.invoice_line:
461 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
462 lines.append(inv_line_id)
463 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
464 inv_id = inv_obj.create(cr, uid, inv, context=context)
465 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
466 if data.get('value', False):
467 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
468 inv_obj.button_compute(cr, uid, [inv_id])
471 def print_quotation(self, cr, uid, ids, context=None):
473 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
475 assert len(ids) == 1, 'This option should only be used for a single id at a time'
476 wf_service = netsvc.LocalService("workflow")
477 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
479 'model': 'sale.order',
481 'form': self.read(cr, uid, ids[0], context=context),
483 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
485 def manual_invoice(self, cr, uid, ids, context=None):
486 mod_obj = self.pool.get('ir.model.data')
487 wf_service = netsvc.LocalService("workflow")
491 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
492 inv_ids.add(record.id)
493 # inv_ids would have old invoices if any
495 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
496 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
497 inv_ids1.add(record.id)
498 inv_ids = list(inv_ids1.difference(inv_ids))
500 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
501 res_id = res and res[1] or False,
504 'name': _('Customer Invoices'),
508 'res_model': 'account.invoice',
509 'context': "{'type':'out_invoice'}",
510 'type': 'ir.actions.act_window',
513 'res_id': inv_ids and inv_ids[0] or False,
516 def action_view_invoice(self, cr, uid, ids, context=None):
518 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
520 mod_obj = self.pool.get('ir.model.data')
522 'name': _('Cutomer Invoice'),
524 'res_model': 'account.invoice',
525 'context': "{'type':'out_invoice', 'journal_type': 'sale'}",
526 'type': 'ir.actions.act_window',
530 #compute the number of invoices to display
532 for so in self.browse(cr, uid, ids, context=context):
533 inv_ids += [invoice.id for invoice in so.invoice_ids]
534 #choose the view_mode accordingly
536 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree')
538 'view_mode': 'tree,form',
539 'res_id': inv_ids or False
542 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
545 'res_id': inv_ids and inv_ids[0] or False,
547 result.update(view_id = res and res[1] or False)
551 def action_view_delivery(self, cr, uid, ids, context=None):
553 This function returns an action that display existing delivery orders of given sale order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
555 mod_obj = self.pool.get('ir.model.data')
557 'name': _('Delivery Order'),
559 'res_model': 'stock.picking',
560 'context': "{'type':'out'}",
561 'type': 'ir.actions.act_window',
565 #compute the number of delivery orders to display
567 for so in self.browse(cr, uid, ids, context=context):
568 pick_ids += [picking.id for picking in so.picking_ids]
569 #choose the view_mode accordingly
570 if len(pick_ids) > 1:
571 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_tree')
573 'view_mode': 'tree,form',
574 'res_id': pick_ids or False
577 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
580 'res_id': pick_ids and pick_ids[0] or False,
582 result.update(view_id = res and res[1] or False)
585 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
589 picking_obj = self.pool.get('stock.picking')
590 invoice = self.pool.get('account.invoice')
591 obj_sale_order_line = self.pool.get('sale.order.line')
592 partner_currency = {}
595 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
596 # last day of the last month as invoice date
598 context['date_inv'] = date_inv
599 for o in self.browse(cr, uid, ids, context=context):
600 currency_id = o.pricelist_id.currency_id.id
601 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
602 raise osv.except_osv(
604 _('You cannot group sales having different currencies for the same partner.'))
606 partner_currency[o.partner_id.id] = currency_id
608 for line in o.order_line:
611 elif (line.state in states):
612 lines.append(line.id)
613 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
615 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
617 for o in self.browse(cr, uid, ids, context=context):
618 for i in o.invoice_ids:
619 if i.state == 'draft':
621 for val in invoices.values():
623 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
626 invoice_ref += o.name + '|'
627 self.write(cr, uid, [o.id], {'state': 'progress'})
628 if o.order_policy == 'picking':
629 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
630 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
631 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
633 for order, il in val:
634 res = self._make_invoice(cr, uid, order, il, context=context)
635 invoice_ids.append(res)
636 self.write(cr, uid, [order.id], {'state': 'progress'})
637 if order.order_policy == 'picking':
638 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
639 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
641 self.invoice_send_note(cr, uid, ids, res, context)
644 def action_invoice_cancel(self, cr, uid, ids, context=None):
647 for sale in self.browse(cr, uid, ids, context=context):
648 for line in sale.order_line:
650 # Check if the line is invoiced (has asociated invoice
651 # lines from non-cancelled invoices).
654 for iline in line.invoice_lines:
655 if iline.invoice_id and iline.invoice_id.state != 'cancel':
658 # Update the line (only when needed)
659 if line.invoiced != invoiced:
660 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
661 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
664 def action_invoice_end(self, cr, uid, ids, context=None):
665 for order in self.browse(cr, uid, ids, context=context):
667 # Update the sale order lines state (and invoiced flag).
669 for line in order.order_line:
672 # Check if the line is invoiced (has asociated invoice
673 # lines from non-cancelled invoices).
676 for iline in line.invoice_lines:
677 if iline.invoice_id and iline.invoice_id.state != 'cancel':
680 if line.invoiced != invoiced:
681 vals['invoiced'] = invoiced
682 # If the line was in exception state, now it gets confirmed.
683 if line.state == 'exception':
684 vals['state'] = 'confirmed'
685 # Update the line (only when needed).
687 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
689 # Update the sales order state.
691 if order.state == 'invoice_except':
692 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
695 def action_cancel(self, cr, uid, ids, context=None):
696 wf_service = netsvc.LocalService("workflow")
699 sale_order_line_obj = self.pool.get('sale.order.line')
700 proc_obj = self.pool.get('procurement.order')
701 for sale in self.browse(cr, uid, ids, context=context):
702 for pick in sale.picking_ids:
703 if pick.state not in ('draft', 'cancel'):
704 raise osv.except_osv(
705 _('Could not cancel sales order!'),
706 _('First cancelled all picking attached to this sale order.'))
707 if pick.state == 'cancel':
708 for mov in pick.move_lines:
709 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
711 for proc in proc_ids:
712 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
713 for r in self.read(cr, uid, ids, ['picking_ids']):
714 for pick in r['picking_ids']:
715 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
716 for inv in sale.invoice_ids:
717 if inv.state not in ('draft', 'cancel'):
718 raise osv.except_osv(
719 _('Could not cancel this sales order!'),
720 _('First cancelled all invoices attached to this sale order.'))
721 for r in self.read(cr, uid, ids, ['invoice_ids']):
722 for inv in r['invoice_ids']:
723 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
724 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
726 self.cancel_send_note(cr, uid, [sale.id], context=None)
727 self.write(cr, uid, ids, {'state': 'cancel'})
730 def action_button_confirm(self, cr, uid, ids, context=None):
731 assert len(ids) == 1, 'This option should only be used for a single id at a time'
732 wf_service = netsvc.LocalService('workflow')
733 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
735 # redisplay the record as a sale order
736 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
737 view_id = view_ref and view_ref[1] or False,
739 'type': 'ir.actions.act_window',
740 'name': _('Sales Order'),
741 'res_model': 'sale.order',
750 def action_wait(self, cr, uid, ids, context=None):
751 for o in self.browse(cr, uid, ids):
753 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
754 if (o.order_policy == 'manual'):
755 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
757 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
758 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
759 self.confirm_send_note(cr, uid, ids, context)
762 def action_quotation_send(self, cr, uid, ids, context=None):
764 This function opens a window to compose an email, with the edi sale template message loaded by default
766 assert len(ids) == 1, 'This option should only be used for a single id at a time'
767 mod_obj = self.pool.get('ir.model.data')
768 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
769 template_id = template and template[1] or False
770 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
771 res_id = res and res[1] or False
772 ctx = dict(context, active_model='sale.order', active_id=ids[0])
773 ctx.update({'mail.compose.template_id': template_id})
777 'res_model': 'mail.compose.message',
778 'views': [(res_id,'form')],
780 'type': 'ir.actions.act_window',
786 def procurement_lines_get(self, cr, uid, ids, *args):
788 for order in self.browse(cr, uid, ids, context={}):
789 for line in order.order_line:
790 if line.procurement_id:
791 res.append(line.procurement_id.id)
794 # if mode == 'finished':
795 # returns True if all lines are done, False otherwise
796 # if mode == 'canceled':
797 # returns True if there is at least one canceled line, False otherwise
798 def test_state(self, cr, uid, ids, mode, *args):
799 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
804 write_cancel_ids = []
805 for order in self.browse(cr, uid, ids, context={}):
806 for line in order.order_line:
807 if (not line.procurement_id) or (line.procurement_id.state=='done'):
808 if line.state != 'done':
809 write_done_ids.append(line.id)
812 if line.procurement_id:
813 if (line.procurement_id.state == 'cancel'):
815 if line.state != 'exception':
816 write_cancel_ids.append(line.id)
820 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
822 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
824 if mode == 'finished':
826 elif mode == 'canceled':
832 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
834 'name': line.name.split('\n')[0],
835 'origin': order.name,
836 'date_planned': date_planned,
837 'product_id': line.product_id.id,
838 'product_qty': line.product_uom_qty,
839 'product_uom': line.product_uom.id,
840 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
841 or line.product_uom_qty,
842 'product_uos': (line.product_uos and line.product_uos.id)\
843 or line.product_uom.id,
844 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
845 'procure_method': line.type,
847 'company_id': order.company_id.id,
848 'note': '\n'.join(line.name.split('\n')[1:]),
849 'property_ids': [(6, 0, [x.id for x in line.property_ids])]
852 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
853 location_id = order.shop_id.warehouse_id.lot_stock_id.id
854 output_id = order.shop_id.warehouse_id.lot_output_id.id
856 'name': line.name.split('\n')[0][:250],
857 'picking_id': picking_id,
858 'product_id': line.product_id.id,
859 'date': date_planned,
860 'date_expected': date_planned,
861 'product_qty': line.product_uom_qty,
862 'product_uom': line.product_uom.id,
863 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
864 'product_uos': (line.product_uos and line.product_uos.id)\
865 or line.product_uom.id,
866 'product_packaging': line.product_packaging.id,
867 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
868 'location_id': location_id,
869 'location_dest_id': output_id,
870 'sale_line_id': line.id,
871 'tracking_id': False,
874 'note': '\n'.join(line.name.split('\n')[1:]),
875 'company_id': order.company_id.id,
876 'price_unit': line.product_id.standard_price or 0.0
879 def _prepare_order_picking(self, cr, uid, order, context=None):
880 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
883 'origin': order.name,
884 'date': order.date_order,
887 'move_type': order.picking_policy,
889 'partner_id': order.partner_shipping_id.id,
891 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
892 'company_id': order.company_id.id,
895 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
896 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
898 Define ship_recreate for process after shipping exception
899 param order: sale order to which the order lines belong
900 param line: sale order line records to procure
901 param move_id: the ID of stock move
902 param proc_id: the ID of procurement
904 move_obj = self.pool.get('stock.move')
905 if order.state == 'shipping_except':
906 for pick in order.picking_ids:
907 for move in pick.move_lines:
908 if move.state == 'cancel':
909 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
911 for mov in move_obj.browse(cr, uid, mov_ids):
912 # 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?
913 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
914 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
917 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
918 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
919 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
922 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
923 """Create the required procurements to supply sale order lines, also connecting
924 the procurements to appropriate stock moves in order to bring the goods to the
925 sale order's requested location.
927 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
928 a standard outgoing picking will be created to wrap the stock moves, as returned
929 by :meth:`~._prepare_order_picking`.
931 Modules that wish to customize the procurements or partition the stock moves over
932 multiple stock pickings may override this method and call ``super()`` with
933 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
935 :param browse_record order: sale order to which the order lines belong
936 :param list(browse_record) order_lines: sale order line records to procure
937 :param int picking_id: optional ID of a stock picking to which the created stock moves
938 will be added. A new picking will be created if ommitted.
941 move_obj = self.pool.get('stock.move')
942 picking_obj = self.pool.get('stock.picking')
943 procurement_obj = self.pool.get('procurement.order')
946 for line in order_lines:
947 if line.state == 'done':
950 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
953 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
955 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
956 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
958 # a service has no stock move
961 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
962 proc_ids.append(proc_id)
963 line.write({'procurement_id': proc_id})
964 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
966 wf_service = netsvc.LocalService("workflow")
968 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
969 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
972 for proc_id in proc_ids:
973 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
976 if order.state == 'shipping_except':
977 val['state'] = 'progress'
978 val['shipped'] = False
980 if (order.order_policy == 'manual'):
981 for line in order.order_line:
982 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
983 val['state'] = 'manual'
988 def action_ship_create(self, cr, uid, ids, context=None):
989 for order in self.browse(cr, uid, ids, context=context):
990 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
993 def action_ship_end(self, cr, uid, ids, context=None):
994 for order in self.browse(cr, uid, ids, context=context):
995 val = {'shipped': True}
996 if order.state == 'shipping_except':
997 val['state'] = 'progress'
998 if (order.order_policy == 'manual'):
999 for line in order.order_line:
1000 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
1001 val['state'] = 'manual'
1003 for line in order.order_line:
1005 if line.state == 'exception':
1006 towrite.append(line.id)
1008 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
1009 res = self.write(cr, uid, [order.id], val)
1011 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1014 def has_stockable_products(self, cr, uid, ids, *args):
1015 for order in self.browse(cr, uid, ids):
1016 for order_line in order.order_line:
1017 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1021 # ------------------------------------------------
1022 # OpenChatter methods and notifications
1023 # ------------------------------------------------
1025 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1026 result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1027 for obj in self.browse(cr, uid, ids, context=context):
1028 if (obj.state == 'manual' or obj.state == 'progress'):
1029 result[obj.id].append(obj.user_id.id)
1032 def create_send_note(self, cr, uid, ids, context=None):
1033 for obj in self.browse(cr, uid, ids, context=context):
1034 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
1035 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)
1037 def confirm_send_note(self, cr, uid, ids, context=None):
1038 for obj in self.browse(cr, uid, ids, context=context):
1039 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)
1041 def cancel_send_note(self, cr, uid, ids, context=None):
1042 for obj in self.browse(cr, uid, ids, context=context):
1043 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1045 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1046 for order in self.browse(cr, uid, ids, context=context):
1047 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1048 # convert datetime field to a datetime, using server format, then
1049 # convert it to the user TZ and re-render it with %Z to add the timezone
1050 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1051 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1052 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)
1054 def delivery_end_send_note(self, cr, uid, ids, context=None):
1055 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1057 def invoice_paid_send_note(self, cr, uid, ids, context=None):
1058 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1060 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1061 for order in self.browse(cr, uid, ids, context=context):
1062 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1063 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)
1065 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1066 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1071 # TODO add a field price_unit_uos
1072 # - update it on change product and unit price
1073 # - use it in report if there is a uos
1074 class sale_order_line(osv.osv):
1076 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1077 tax_obj = self.pool.get('account.tax')
1078 cur_obj = self.pool.get('res.currency')
1082 for line in self.browse(cr, uid, ids, context=context):
1083 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1084 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1085 cur = line.order_id.pricelist_id.currency_id
1086 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1089 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1091 for line in self.browse(cr, uid, ids, context=context):
1093 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1098 def _get_uom_id(self, cr, uid, *args):
1100 proxy = self.pool.get('ir.model.data')
1101 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1103 except Exception, ex:
1106 _name = 'sale.order.line'
1107 _description = 'Sales Order Line'
1109 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1110 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1111 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1112 '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)]}),
1113 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1114 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1115 'invoiced': fields.boolean('Invoiced', readonly=True),
1116 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1117 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1118 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1119 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1120 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1121 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."),
1122 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1123 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1124 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1125 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1126 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1127 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1128 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1129 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1130 'discount': fields.float('Discount', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1131 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1132 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1133 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1134 help='* The \'Draft\' state is set when the related sales order in draft state. \
1135 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1136 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1137 \n* The \'Done\' state is set when the sales order line has been picked. \
1138 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1139 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1140 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1141 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1143 _order = 'sequence, id'
1145 'product_uom' : _get_uom_id,
1148 'product_uom_qty': 1,
1149 'product_uos_qty': 1,
1153 'type': 'make_to_stock',
1154 'product_packaging': False,
1158 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1159 """Prepare the dict of values to create the new invoice line for a
1160 sale order line. This method may be overridden to implement custom
1161 invoice generation (making sure to call super() to establish
1162 a clean extension chain).
1164 :param browse_record line: sale.order.line record to invoice
1165 :param int account_id: optional ID of a G/L account to force
1166 (this is used for returning products including service)
1167 :return: dict of values to create() the invoice line
1170 def _get_line_qty(line):
1171 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1172 if line.product_uos:
1173 return line.product_uos_qty or 0.0
1174 return line.product_uom_qty
1176 return self.pool.get('procurement.order').quantity_get(cr, uid,
1177 line.procurement_id.id, context=context)
1179 def _get_line_uom(line):
1180 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1181 if line.product_uos:
1182 return line.product_uos.id
1183 return line.product_uom.id
1185 return self.pool.get('procurement.order').uom_get(cr, uid,
1186 line.procurement_id.id, context=context)
1188 if not line.invoiced:
1191 account_id = line.product_id.product_tmpl_id.property_account_income.id
1193 account_id = line.product_id.categ_id.property_account_income_categ.id
1195 raise osv.except_osv(_('Error !'),
1196 _('There is no income account defined for this product: "%s" (id:%d)') % \
1197 (line.product_id.name, line.product_id.id,))
1199 prop = self.pool.get('ir.property').get(cr, uid,
1200 'property_account_income_categ', 'product.category',
1202 account_id = prop and prop.id or False
1203 uosqty = _get_line_qty(line)
1204 uos_id = _get_line_uom(line)
1207 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1208 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1209 fpos = line.order_id.fiscal_position or False
1210 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1212 raise osv.except_osv(_('Error !'),
1213 _('There is no Fiscal Position defined or income category account defined for Product Categories default Properties.'))
1216 'origin': line.order_id.name,
1217 'account_id': account_id,
1220 'discount': line.discount,
1222 'product_id': line.product_id.id or False,
1223 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1224 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1229 def invoice_line_create(self, cr, uid, ids, context=None):
1235 for line in self.browse(cr, uid, ids, context=context):
1236 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1238 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1239 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1240 self.write(cr, uid, [line.id], {'invoiced': True})
1241 sales.add(line.order_id.id)
1242 create_ids.append(inv_id)
1243 # Trigger workflow events
1244 wf_service = netsvc.LocalService("workflow")
1245 for sale_id in sales:
1246 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1249 def button_cancel(self, cr, uid, ids, context=None):
1250 for line in self.browse(cr, uid, ids, context=context):
1252 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1253 for move_line in line.move_ids:
1254 if move_line.state != 'cancel':
1255 raise osv.except_osv(
1256 _('Cannot cancel sale order line!'),
1257 _('You must first cancel stock moves attached to this sales order line.'))
1258 return self.write(cr, uid, ids, {'state': 'cancel'})
1260 def button_confirm(self, cr, uid, ids, context=None):
1261 return self.write(cr, uid, ids, {'state': 'confirmed'})
1263 def button_done(self, cr, uid, ids, context=None):
1264 wf_service = netsvc.LocalService("workflow")
1265 res = self.write(cr, uid, ids, {'state': 'done'})
1266 for line in self.browse(cr, uid, ids, context=context):
1267 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1270 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1271 product_obj = self.pool.get('product.product')
1273 return {'value': {'product_uom': product_uos,
1274 'product_uom_qty': product_uos_qty}, 'domain': {}}
1276 product = product_obj.browse(cr, uid, product_id)
1278 'product_uom': product.uom_id.id,
1280 # FIXME must depend on uos/uom of the product and not only of the coeff.
1283 'product_uom_qty': product_uos_qty / product.uos_coeff,
1284 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1286 except ZeroDivisionError:
1288 return {'value': value}
1290 def copy_data(self, cr, uid, id, default=None, context=None):
1293 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1294 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1296 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1297 partner_id=False, packaging=False, flag=False, context=None):
1299 return {'value': {'product_packaging': False}}
1300 product_obj = self.pool.get('product.product')
1301 product_uom_obj = self.pool.get('product.uom')
1302 pack_obj = self.pool.get('product.packaging')
1307 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1308 product=product, qty=qty, uom=uom, partner_id=partner_id,
1309 packaging=packaging, flag=False, context=context)
1310 warning_msgs = res.get('warning') and res['warning']['message']
1312 products = product_obj.browse(cr, uid, product, context=context)
1313 if not products.packaging:
1314 packaging = result['product_packaging'] = False
1315 elif not packaging and products.packaging and not flag:
1316 packaging = products.packaging[0].id
1317 result['product_packaging'] = packaging
1320 default_uom = products.uom_id and products.uom_id.id
1321 pack = pack_obj.browse(cr, uid, packaging, context=context)
1322 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1323 # qty = qty - qty % q + q
1324 if qty and (q and not (qty % q) == 0):
1325 ean = pack.ean or _('(n/a)')
1328 if not warning_msgs:
1329 warn_msg = _("You selected a quantity of %d Units.\n"
1330 "But it's not compatible with the selected packaging.\n"
1331 "Here is a proposition of quantities according to the packaging:\n"
1332 "EAN: %s Quantity: %s Type of ul: %s") % \
1333 (qty, ean, qty_pack, type_ul.name)
1334 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1336 'title': _('Configuration Error !'),
1337 'message': warning_msgs
1339 result['product_uom_qty'] = qty
1341 return {'value': result, 'warning': warning}
1343 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1344 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1345 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1346 context = context or {}
1347 lang = lang or context.get('lang',False)
1349 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1351 product_uom_obj = self.pool.get('product.uom')
1352 partner_obj = self.pool.get('res.partner')
1353 product_obj = self.pool.get('product.product')
1354 context = {'lang': lang, 'partner_id': partner_id}
1356 lang = partner_obj.browse(cr, uid, partner_id).lang
1357 context_partner = {'lang': lang, 'partner_id': partner_id}
1360 return {'value': {'th_weight': 0, 'product_packaging': False,
1361 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1364 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1366 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1367 result = res.get('value', {})
1368 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1369 product_obj = product_obj.browse(cr, uid, product, context=context)
1373 uom2 = product_uom_obj.browse(cr, uid, uom)
1374 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1377 if product_obj.uos_id:
1378 uos2 = product_uom_obj.browse(cr, uid, uos)
1379 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1383 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1384 if update_tax: #The quantity only have changed
1385 result['delay'] = (product_obj.sale_delay or 0.0)
1386 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1387 result.update({'type': product_obj.procure_method})
1390 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1391 if product_obj.description_sale:
1392 result['name'] += '\n'+product_obj.description_sale
1394 if (not uom) and (not uos):
1395 result['product_uom'] = product_obj.uom_id.id
1396 if product_obj.uos_id:
1397 result['product_uos'] = product_obj.uos_id.id
1398 result['product_uos_qty'] = qty * product_obj.uos_coeff
1399 uos_category_id = product_obj.uos_id.category_id.id
1401 result['product_uos'] = False
1402 result['product_uos_qty'] = qty
1403 uos_category_id = False
1404 result['th_weight'] = qty * product_obj.weight
1405 domain = {'product_uom':
1406 [('category_id', '=', product_obj.uom_id.category_id.id)],
1408 [('category_id', '=', uos_category_id)]}
1410 elif uos and not uom: # only happens if uom is False
1411 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1412 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1413 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1414 elif uom: # whether uos is set or not
1415 default_uom = product_obj.uom_id and product_obj.uom_id.id
1416 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1417 if product_obj.uos_id:
1418 result['product_uos'] = product_obj.uos_id.id
1419 result['product_uos_qty'] = qty * product_obj.uos_coeff
1421 result['product_uos'] = False
1422 result['product_uos_qty'] = qty
1423 result['th_weight'] = q * product_obj.weight # Round the quantity up
1426 uom2 = product_obj.uom_id
1427 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1428 if (product_obj.type=='product') and int(compare_qty) == -1 \
1429 and (product_obj.procure_method=='make_to_stock'):
1430 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1431 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1432 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1433 max(0,product_obj.qty_available), product_obj.uom_id.name)
1434 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1438 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1439 'Please set one before choosing a product.')
1440 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1442 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1443 product, qty or 1.0, partner_id, {
1444 'uom': uom or result.get('product_uom'),
1448 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1449 "You have to change either the product, the quantity or the pricelist.")
1451 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1453 result.update({'price_unit': price})
1456 'title': _('Configuration Error !'),
1457 'message' : warning_msgs
1459 return {'value': result, 'domain': domain, 'warning': warning}
1461 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1462 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1463 lang=False, update_tax=True, date_order=False, context=None):
1464 context = context or {}
1465 lang = lang or ('lang' in context and context['lang'])
1466 res = self.product_id_change(cursor, user, ids, pricelist, product,
1467 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1468 partner_id=partner_id, lang=lang, update_tax=update_tax,
1469 date_order=date_order, context=context)
1470 if 'product_uom' in res['value']:
1471 del res['value']['product_uom']
1473 res['value']['price_unit'] = 0.0
1476 def unlink(self, cr, uid, ids, context=None):
1479 """Allows to delete sales order lines in draft,cancel states"""
1480 for rec in self.browse(cr, uid, ids, context=context):
1481 if rec.state not in ['draft', 'cancel']:
1482 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1483 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1487 class mail_message(osv.osv):
1488 _inherit = 'mail.message'
1490 def _postprocess_sent_message(self, cr, uid, message, context=None):
1491 if message.model == 'sale.order':
1492 wf_service = netsvc.LocalService("workflow")
1493 wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1494 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1498 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: