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
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
27 import openerp.addons.decimal_precision as dp
28 from openerp import workflow
30 class res_company(osv.Model):
31 _inherit = "res.company"
33 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
36 class sale_order(osv.osv):
38 _inherit = ['mail.thread', 'ir.needaction_mixin']
39 _description = "Sales Order"
42 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
43 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
47 def _amount_line_tax(self, cr, uid, line, context=None):
49 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']:
50 val += c.get('amount', 0.0)
53 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
54 """ Wrapper because of direct method passing as parameter for function fields """
55 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
57 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
58 cur_obj = self.pool.get('res.currency')
60 for order in self.browse(cr, uid, ids, context=context):
62 'amount_untaxed': 0.0,
67 cur = order.pricelist_id.currency_id
68 for line in order.order_line:
69 val1 += line.price_subtotal
70 val += self._amount_line_tax(cr, uid, line, context=context)
71 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
72 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
73 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
77 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
79 for sale in self.browse(cursor, user, ids, context=context):
84 for invoice in sale.invoice_ids:
85 if invoice.state not in ('draft', 'cancel'):
86 tot += invoice.amount_untaxed
88 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
93 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
95 for sale in self.browse(cursor, user, ids, context=context):
101 def _invoiced(self, cursor, user, ids, name, arg, context=None):
103 for sale in self.browse(cursor, user, ids, context=context):
105 invoice_existence = False
106 for invoice in sale.invoice_ids:
107 if invoice.state!='cancel':
108 invoice_existence = True
109 if invoice.state != 'paid':
112 if not invoice_existence or sale.state == 'manual':
116 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
123 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
124 clause += 'AND inv.state = \'paid\''
126 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
127 sale_clause = ', sale_order AS sale '
130 cursor.execute('SELECT rel.order_id ' \
131 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
132 'WHERE rel.invoice_id = inv.id ' + clause)
133 res = cursor.fetchall()
135 cursor.execute('SELECT sale.id ' \
136 'FROM sale_order AS sale ' \
137 'WHERE sale.id NOT IN ' \
138 '(SELECT rel.order_id ' \
139 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
140 res.extend(cursor.fetchall())
142 return [('id', '=', 0)]
143 return [('id', 'in', [x[0] for x in res])]
145 def _get_order(self, cr, uid, ids, context=None):
147 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
148 result[line.order_id.id] = True
151 def _get_default_company(self, cr, uid, context=None):
152 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
154 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
157 def _get_default_section_id(self, cr, uid, context=None):
158 """ Gives default section by checking if present in the context """
159 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
161 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
164 def _resolve_section_id_from_context(self, cr, uid, context=None):
165 """ Returns ID of section based on the value of 'section_id'
166 context key, or None if it cannot be resolved to a single
171 if type(context.get('default_section_id')) in (int, long):
172 return context.get('default_section_id')
173 if isinstance(context.get('default_section_id'), basestring):
174 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
175 if len(section_ids) == 1:
176 return int(section_ids[0][0])
180 'name': fields.char('Order Reference', required=True, copy=False,
181 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
182 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
183 'client_order_ref': fields.char('Reference/Description', copy=False),
184 'state': fields.selection([
185 ('draft', 'Draft Quotation'),
186 ('sent', 'Quotation Sent'),
187 ('cancel', 'Cancelled'),
188 ('waiting_date', 'Waiting Schedule'),
189 ('progress', 'Sales Order'),
190 ('manual', 'Sale to Invoice'),
191 ('shipping_except', 'Shipping Exception'),
192 ('invoice_except', 'Invoice Exception'),
194 ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
195 \nThe exception status is automatically set when a cancel operation occurs \
196 in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
197 but waiting for the scheduler to run on the order date.", select=True),
198 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
199 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
200 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
201 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
202 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
203 '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."),
204 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
205 'order_policy': fields.selection([
206 ('manual', 'On Demand'),
207 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
208 help="""This field controls how invoice and delivery operations are synchronized."""),
209 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
210 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
211 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
213 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
214 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, 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)."),
215 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
216 'invoiced': fields.function(_invoiced, string='Paid',
217 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
218 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
219 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
220 'note': fields.text('Terms and conditions'),
222 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
224 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
227 multi='sums', help="The amount without tax.", track_visibility='always'),
228 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
230 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
231 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
233 multi='sums', help="The tax amount."),
234 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
236 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
237 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
239 multi='sums', help="The total amount."),
241 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
242 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
243 'company_id': fields.many2one('res.company', 'Company'),
244 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
245 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
246 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
250 'date_order': fields.datetime.now,
251 'order_policy': 'manual',
252 'company_id': _get_default_company,
254 'user_id': lambda obj, cr, uid, context: uid,
255 'name': lambda obj, cr, uid, context: '/',
256 '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'],
257 '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'],
258 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
259 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
262 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
264 _order = 'date_order desc, id desc'
267 def unlink(self, cr, uid, ids, context=None):
268 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
270 for s in sale_orders:
271 if s['state'] in ['draft', 'cancel']:
272 unlink_ids.append(s['id'])
274 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
276 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
278 def copy_quotation(self, cr, uid, ids, context=None):
279 id = self.copy(cr, uid, ids[0], context=context)
280 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
281 view_id = view_ref and view_ref[1] or False,
283 'type': 'ir.actions.act_window',
284 'name': _('Sales Order'),
285 'res_model': 'sale.order',
294 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
295 context = context or {}
299 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
301 if not order_lines or order_lines == [(6, 0, [])]:
302 return {'value': value}
304 'title': _('Pricelist Warning!'),
305 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
307 return {'warning': warning, 'value': value}
309 def get_salenote(self, cr, uid, ids, partner_id, context=None):
310 context_lang = context.copy()
312 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
313 context_lang.update({'lang': partner_lang})
314 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
316 def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
318 if not fiscal_position:
320 company_id = self._get_default_company(cr, uid, context=context)
321 fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
323 r['value']['fiscal_position'] = fiscal_position
326 def onchange_partner_id(self, cr, uid, ids, part, context=None):
328 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
330 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
331 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
332 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
333 payment_term = part.property_payment_term and part.property_payment_term.id or False
334 dedicated_salesman = part.user_id and part.user_id.id or uid
336 'partner_invoice_id': addr['invoice'],
337 'partner_shipping_id': addr['delivery'],
338 'payment_term': payment_term,
339 'user_id': dedicated_salesman,
341 delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
342 val.update(delivery_onchange['value'])
344 val['pricelist_id'] = pricelist
345 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
346 if sale_note: val.update({'note': sale_note})
347 return {'value': val}
349 def create(self, cr, uid, vals, context=None):
352 if vals.get('name', '/') == '/':
353 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
354 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']):
355 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
356 if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
357 delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
358 defaults.update(delivery_onchange['value'])
359 vals = dict(defaults, **vals)
360 ctx = dict(context or {}, mail_create_nolog=True)
361 new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
362 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
365 def button_dummy(self, cr, uid, ids, context=None):
368 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
369 # can be removed after 6.1.
370 def _inv_get(self, cr, uid, order, context=None):
373 def _prepare_invoice(self, cr, uid, order, lines, context=None):
374 """Prepare the dict of values to create the new invoice for a
375 sales order. This method may be overridden to implement custom
376 invoice generation (making sure to call super() to establish
377 a clean extension chain).
379 :param browse_record order: sale.order record to invoice
380 :param list(int) line: list of invoice line IDs that must be
381 attached to the invoice
382 :return: dict of value to create() the invoice
386 journal_ids = self.pool.get('account.journal').search(cr, uid,
387 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
390 raise osv.except_osv(_('Error!'),
391 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
393 'name': order.client_order_ref or '',
394 'origin': order.name,
395 'type': 'out_invoice',
396 'reference': order.client_order_ref or order.name,
397 'account_id': order.partner_id.property_account_receivable.id,
398 'partner_id': order.partner_invoice_id.id,
399 'journal_id': journal_ids[0],
400 'invoice_line': [(6, 0, lines)],
401 'currency_id': order.pricelist_id.currency_id.id,
402 'comment': order.note,
403 'payment_term': order.payment_term and order.payment_term.id or False,
404 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
405 'date_invoice': context.get('date_invoice', False),
406 'company_id': order.company_id.id,
407 'user_id': order.user_id and order.user_id.id or False,
408 'section_id' : order.section_id.id
411 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
412 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
415 def _make_invoice(self, cr, uid, order, lines, context=None):
416 inv_obj = self.pool.get('account.invoice')
417 obj_invoice_line = self.pool.get('account.invoice.line')
420 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
421 from_line_invoice_ids = []
422 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
423 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
424 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
425 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
426 for preinv in order.invoice_ids:
427 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
428 for preline in preinv.invoice_line:
429 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
430 lines.append(inv_line_id)
431 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
432 inv_id = inv_obj.create(cr, uid, inv, context=context)
433 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
434 if data.get('value', False):
435 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
436 inv_obj.button_compute(cr, uid, [inv_id])
439 def print_quotation(self, cr, uid, ids, context=None):
441 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
443 assert len(ids) == 1, 'This option should only be used for a single id at a time'
444 self.signal_workflow(cr, uid, ids, 'quotation_sent')
445 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
447 def manual_invoice(self, cr, uid, ids, context=None):
448 """ create invoices for the given sales orders (ids), and open the form
449 view of one of the newly created invoices
451 mod_obj = self.pool.get('ir.model.data')
453 # create invoices through the sales orders' workflow
454 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
455 self.signal_workflow(cr, uid, ids, 'manual_invoice')
456 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
457 # determine newly created invoices
458 new_inv_ids = list(inv_ids1 - inv_ids0)
460 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
461 res_id = res and res[1] or False,
464 'name': _('Customer Invoices'),
468 'res_model': 'account.invoice',
469 'context': "{'type':'out_invoice'}",
470 'type': 'ir.actions.act_window',
473 'res_id': new_inv_ids and new_inv_ids[0] or False,
476 def action_view_invoice(self, cr, uid, ids, context=None):
478 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
480 mod_obj = self.pool.get('ir.model.data')
481 act_obj = self.pool.get('ir.actions.act_window')
483 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
484 id = result and result[1] or False
485 result = act_obj.read(cr, uid, [id], context=context)[0]
486 #compute the number of invoices to display
488 for so in self.browse(cr, uid, ids, context=context):
489 inv_ids += [invoice.id for invoice in so.invoice_ids]
490 #choose the view_mode accordingly
492 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
494 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
495 result['views'] = [(res and res[1] or False, 'form')]
496 result['res_id'] = inv_ids and inv_ids[0] or False
499 def test_no_product(self, cr, uid, order, context):
500 for line in order.order_line:
501 if line.product_id and (line.product_id.type<>'service'):
505 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
507 states = ['confirmed', 'done', 'exception']
511 invoice = self.pool.get('account.invoice')
512 obj_sale_order_line = self.pool.get('sale.order.line')
513 partner_currency = {}
514 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
515 # last day of the last month as invoice date
517 context = dict(context or {}, date_invoice=date_invoice)
518 for o in self.browse(cr, uid, ids, context=context):
519 currency_id = o.pricelist_id.currency_id.id
520 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
521 raise osv.except_osv(
523 _('You cannot group sales having different currencies for the same partner.'))
525 partner_currency[o.partner_id.id] = currency_id
527 for line in o.order_line:
530 elif (line.state in states):
531 lines.append(line.id)
532 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
534 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
536 for o in self.browse(cr, uid, ids, context=context):
537 for i in o.invoice_ids:
538 if i.state == 'draft':
540 for val in invoices.values():
542 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
546 invoice_ref += (o.client_order_ref or o.name) + '|'
547 origin_ref += (o.origin or o.name) + '|'
548 self.write(cr, uid, [o.id], {'state': 'progress'})
549 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
550 self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
551 #remove last '|' in invoice_ref
552 if len(invoice_ref) >= 1:
553 invoice_ref = invoice_ref[:-1]
554 if len(origin_ref) >= 1:
555 origin_ref = origin_ref[:-1]
556 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
558 for order, il in val:
559 res = self._make_invoice(cr, uid, order, il, context=context)
560 invoice_ids.append(res)
561 self.write(cr, uid, [order.id], {'state': 'progress'})
562 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
563 self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
566 def action_invoice_cancel(self, cr, uid, ids, context=None):
567 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
570 def action_invoice_end(self, cr, uid, ids, context=None):
571 for this in self.browse(cr, uid, ids, context=context):
572 for line in this.order_line:
573 if line.state == 'exception':
574 line.write({'state': 'confirmed'})
575 if this.state == 'invoice_except':
576 this.write({'state': 'progress'})
579 def action_cancel(self, cr, uid, ids, context=None):
582 sale_order_line_obj = self.pool.get('sale.order.line')
583 account_invoice_obj = self.pool.get('account.invoice')
584 for sale in self.browse(cr, uid, ids, context=context):
585 for inv in sale.invoice_ids:
586 if inv.state not in ('draft', 'cancel'):
587 raise osv.except_osv(
588 _('Cannot cancel this sales order!'),
589 _('First cancel all invoices attached to this sales order.'))
590 inv.signal_workflow('invoice_cancel')
591 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
593 self.write(cr, uid, ids, {'state': 'cancel'})
596 def action_button_confirm(self, cr, uid, ids, context=None):
597 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
598 self.signal_workflow(cr, uid, ids, 'order_confirm')
601 def action_wait(self, cr, uid, ids, context=None):
602 context = context or {}
603 for o in self.browse(cr, uid, ids):
605 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
606 noprod = self.test_no_product(cr, uid, o, context)
607 if (o.order_policy == 'manual') or noprod:
608 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
610 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
611 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
614 def action_quotation_send(self, cr, uid, ids, context=None):
616 This function opens a window to compose an email, with the edi sale template message loaded by default
618 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
619 ir_model_data = self.pool.get('ir.model.data')
621 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
625 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
627 compose_form_id = False
630 'default_model': 'sale.order',
631 'default_res_id': ids[0],
632 'default_use_template': bool(template_id),
633 'default_template_id': template_id,
634 'default_composition_mode': 'comment',
635 'mark_so_as_sent': True
638 'type': 'ir.actions.act_window',
641 'res_model': 'mail.compose.message',
642 'views': [(compose_form_id, 'form')],
643 'view_id': compose_form_id,
648 def action_done(self, cr, uid, ids, context=None):
649 for order in self.browse(cr, uid, ids, context=context):
650 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
651 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
653 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
654 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
657 'origin': order.name,
658 'date_planned': date_planned,
659 'product_id': line.product_id.id,
660 'product_qty': line.product_uom_qty,
661 'product_uom': line.product_uom.id,
662 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
663 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
664 'company_id': order.company_id.id,
665 'group_id': group_id,
666 'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
667 'sale_line_id': line.id
670 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
671 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
674 def _prepare_procurement_group(self, cr, uid, order, context=None):
675 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
677 def procurement_needed(self, cr, uid, ids, context=None):
678 #when sale is installed only, there is no need to create procurements, that's only
679 #further installed modules (sale_service, sale_stock) that will change this.
680 sale_line_obj = self.pool.get('sale.order.line')
682 for order in self.browse(cr, uid, ids, context=context):
683 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
686 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
687 for sale_order in self.browse(cr, uid, ids, context=context):
688 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
691 def action_ship_create(self, cr, uid, ids, context=None):
692 """Create the required procurements to supply sales order lines, also connecting
693 the procurements to appropriate stock moves in order to bring the goods to the
694 sales order's requested location.
698 procurement_obj = self.pool.get('procurement.order')
699 sale_line_obj = self.pool.get('sale.order.line')
700 for order in self.browse(cr, uid, ids, context=context):
702 vals = self._prepare_procurement_group(cr, uid, order, context=context)
703 if not order.procurement_group_id:
704 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
705 order.write({'procurement_group_id': group_id})
707 for line in order.order_line:
708 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
709 if line.procurement_ids:
710 #first check them to see if they are in exception or not (one of the related moves is cancelled)
711 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
713 #run again procurement that are in exception in order to trigger another move
714 proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
715 procurement_obj.reset_to_confirmed(cr, uid, proc_ids, context=context)
716 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
717 if (line.state == 'done') or not line.product_id:
719 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
720 proc_id = procurement_obj.create(cr, uid, vals, context=context)
721 proc_ids.append(proc_id)
722 #Confirm procurement order such that rules will be applied on it
723 #note that the workflow normally ensure proc_ids isn't an empty list
724 procurement_obj.run(cr, uid, proc_ids, context=context)
726 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
727 if order.state == 'shipping_except':
728 val = {'state': 'progress', 'shipped': False}
730 if (order.order_policy == 'manual'):
731 for line in order.order_line:
732 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
733 val['state'] = 'manual'
740 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
741 '''Update taxes of order lines for each line where a product is defined
743 :param list ids: not used
744 :param int fiscal_position: sale order fiscal position
745 :param list order_lines: command list for one2many write method
748 fiscal_obj = self.pool.get('account.fiscal.position')
749 product_obj = self.pool.get('product.product')
750 line_obj = self.pool.get('sale.order.line')
754 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
756 for line in order_lines:
757 # create (0, 0, { fields })
758 # update (1, ID, { fields })
759 if line[0] in [0, 1]:
761 if line[2].get('product_id'):
762 prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
764 prod = line_obj.browse(cr, uid, line[1], context=context).product_id
765 if prod and prod.taxes_id:
766 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
767 order_line.append(line)
770 # link all (6, 0, IDS)
771 elif line[0] in [4, 6]:
772 line_ids = line[0] == 4 and [line[1]] or line[2]
773 for line_id in line_ids:
774 prod = line_obj.browse(cr, uid, line_id, context=context).product_id
775 if prod and prod.taxes_id:
776 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
778 order_line.append([4, line_id])
780 order_line.append(line)
781 return {'value': {'order_line': order_line}}
783 def test_procurements_done(self, cr, uid, ids, context=None):
784 for sale in self.browse(cr, uid, ids, context=context):
785 for line in sale.order_line:
786 if not all([x.state == 'done' for x in line.procurement_ids]):
790 def test_procurements_except(self, cr, uid, ids, context=None):
791 for sale in self.browse(cr, uid, ids, context=context):
792 for line in sale.order_line:
793 if any([x.state == 'cancel' for x in line.procurement_ids]):
798 # TODO add a field price_unit_uos
799 # - update it on change product and unit price
800 # - use it in report if there is a uos
801 class sale_order_line(osv.osv):
803 def need_procurement(self, cr, uid, ids, context=None):
804 #when sale is installed only, there is no need to create procurements, that's only
805 #further installed modules (sale_service, sale_stock) that will change this.
806 prod_obj = self.pool.get('product.product')
807 for line in self.browse(cr, uid, ids, context=context):
808 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
812 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
813 tax_obj = self.pool.get('account.tax')
814 cur_obj = self.pool.get('res.currency')
818 for line in self.browse(cr, uid, ids, context=context):
819 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
820 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
821 cur = line.order_id.pricelist_id.currency_id
822 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
825 def _get_uom_id(self, cr, uid, *args):
827 proxy = self.pool.get('ir.model.data')
828 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
830 except Exception, ex:
833 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
834 res = dict.fromkeys(ids, False)
835 for this in self.browse(cr, uid, ids, context=context):
836 res[this.id] = this.invoice_lines and \
837 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
840 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
841 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
842 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
843 sale_order_line sol ON (sol.order_id = rel.order_id)
844 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
845 return [i[0] for i in cr.fetchall()]
847 def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None):
848 res = dict.fromkeys(ids, 0.0)
849 for line in self.browse(cr, uid, ids, context=context):
850 res[line.id] = line.price_subtotal / line.product_uom_qty
853 _name = 'sale.order.line'
854 _description = 'Sales Order Line'
856 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
857 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
858 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
859 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
860 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
861 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
863 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
864 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
866 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
867 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
868 'price_reduce': fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product Price')),
869 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
870 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
871 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
872 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
873 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
874 'product_uos': fields.many2one('product.uom', 'Product UoS'),
875 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
876 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
877 'state': fields.selection(
878 [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
879 'Status', required=True, readonly=True, copy=False,
880 help='* The \'Draft\' status is set when the related sales order in draft status. \
881 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
882 \n* The \'Exception\' status is set when the related sales order is set as exception. \
883 \n* The \'Done\' status is set when the sales order line has been picked. \
884 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
885 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
886 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
887 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
888 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
889 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
891 _order = 'order_id desc, sequence, id'
893 'product_uom' : _get_uom_id,
895 'product_uom_qty': 1,
896 'product_uos_qty': 1,
905 def _get_line_qty(self, cr, uid, line, context=None):
907 return line.product_uos_qty or 0.0
908 return line.product_uom_qty
910 def _get_line_uom(self, cr, uid, line, context=None):
912 return line.product_uos.id
913 return line.product_uom.id
915 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
916 """Prepare the dict of values to create the new invoice line for a
917 sales order line. This method may be overridden to implement custom
918 invoice generation (making sure to call super() to establish
919 a clean extension chain).
921 :param browse_record line: sale.order.line record to invoice
922 :param int account_id: optional ID of a G/L account to force
923 (this is used for returning products including service)
924 :return: dict of values to create() the invoice line
927 if not line.invoiced:
930 account_id = line.product_id.property_account_income.id
932 account_id = line.product_id.categ_id.property_account_income_categ.id
934 raise osv.except_osv(_('Error!'),
935 _('Please define income account for this product: "%s" (id:%d).') % \
936 (line.product_id.name, line.product_id.id,))
938 prop = self.pool.get('ir.property').get(cr, uid,
939 'property_account_income_categ', 'product.category',
941 account_id = prop and prop.id or False
942 uosqty = self._get_line_qty(cr, uid, line, context=context)
943 uos_id = self._get_line_uom(cr, uid, line, context=context)
946 pu = round(line.price_unit * line.product_uom_qty / uosqty,
947 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
948 fpos = line.order_id.fiscal_position or False
949 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
951 raise osv.except_osv(_('Error!'),
952 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
955 'sequence': line.sequence,
956 'origin': line.order_id.name,
957 'account_id': account_id,
960 'discount': line.discount,
962 'product_id': line.product_id.id or False,
963 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
964 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
969 def invoice_line_create(self, cr, uid, ids, context=None):
975 for line in self.browse(cr, uid, ids, context=context):
976 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
978 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
979 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
980 sales.add(line.order_id.id)
981 create_ids.append(inv_id)
982 # Trigger workflow events
983 for sale_id in sales:
984 workflow.trg_write(uid, 'sale.order', sale_id, cr)
987 def button_cancel(self, cr, uid, ids, context=None):
988 for line in self.browse(cr, uid, ids, context=context):
990 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
991 return self.write(cr, uid, ids, {'state': 'cancel'})
993 def button_confirm(self, cr, uid, ids, context=None):
994 return self.write(cr, uid, ids, {'state': 'confirmed'})
996 def button_done(self, cr, uid, ids, context=None):
997 res = self.write(cr, uid, ids, {'state': 'done'})
998 for line in self.browse(cr, uid, ids, context=context):
999 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
1002 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1003 product_obj = self.pool.get('product.product')
1005 return {'value': {'product_uom': product_uos,
1006 'product_uom_qty': product_uos_qty}, 'domain': {}}
1008 product = product_obj.browse(cr, uid, product_id)
1010 'product_uom': product.uom_id.id,
1012 # FIXME must depend on uos/uom of the product and not only of the coeff.
1015 'product_uom_qty': product_uos_qty / product.uos_coeff,
1016 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1018 except ZeroDivisionError:
1020 return {'value': value}
1022 def create(self, cr, uid, values, context=None):
1023 if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
1024 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1025 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1026 qty=float(values.get('product_uom_qty', False)),
1027 uom=values.get('product_uom', False),
1028 qty_uos=float(values.get('product_uos_qty', False)),
1029 uos=values.get('product_uos', False),
1030 name=values.get('name', False),
1031 partner_id=order['partner_id'][0],
1032 date_order=order['date_order'],
1033 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1034 flag=False, # Force name update
1037 if defaults.get('tax_id'):
1038 defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
1039 values = dict(defaults, **values)
1040 return super(sale_order_line, self).create(cr, uid, values, context=context)
1042 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1043 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1044 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1045 context = context or {}
1046 lang = lang or context.get('lang', False)
1048 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
1050 product_uom_obj = self.pool.get('product.uom')
1051 partner_obj = self.pool.get('res.partner')
1052 product_obj = self.pool.get('product.product')
1053 context = {'lang': lang, 'partner_id': partner_id}
1054 partner = partner_obj.browse(cr, uid, partner_id)
1056 context_partner = {'lang': lang, 'partner_id': partner_id}
1059 return {'value': {'th_weight': 0,
1060 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1063 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1067 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
1071 uom2 = product_uom_obj.browse(cr, uid, uom)
1072 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1075 if product_obj.uos_id:
1076 uos2 = product_uom_obj.browse(cr, uid, uos)
1077 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1083 if not fiscal_position:
1084 fpos = partner.property_account_position or False
1086 fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position)
1087 if update_tax: #The quantity only have changed
1088 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1091 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1092 if product_obj.description_sale:
1093 result['name'] += '\n'+product_obj.description_sale
1095 if (not uom) and (not uos):
1096 result['product_uom'] = product_obj.uom_id.id
1097 if product_obj.uos_id:
1098 result['product_uos'] = product_obj.uos_id.id
1099 result['product_uos_qty'] = qty * product_obj.uos_coeff
1100 uos_category_id = product_obj.uos_id.category_id.id
1102 result['product_uos'] = False
1103 result['product_uos_qty'] = qty
1104 uos_category_id = False
1105 result['th_weight'] = qty * product_obj.weight
1106 domain = {'product_uom':
1107 [('category_id', '=', product_obj.uom_id.category_id.id)],
1109 [('category_id', '=', uos_category_id)]}
1110 elif uos and not uom: # only happens if uom is False
1111 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1112 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1113 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1114 elif uom: # whether uos is set or not
1115 default_uom = product_obj.uom_id and product_obj.uom_id.id
1116 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1117 if product_obj.uos_id:
1118 result['product_uos'] = product_obj.uos_id.id
1119 result['product_uos_qty'] = qty * product_obj.uos_coeff
1121 result['product_uos'] = False
1122 result['product_uos_qty'] = qty
1123 result['th_weight'] = q * product_obj.weight # Round the quantity up
1126 uom2 = product_obj.uom_id
1130 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1131 'Please set one before choosing a product.')
1132 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1134 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1135 product, qty or 1.0, partner_id, {
1136 'uom': uom or result.get('product_uom'),
1140 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1141 "You have to change either the product, the quantity or the pricelist.")
1143 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1145 result.update({'price_unit': price})
1148 'title': _('Configuration Error!'),
1149 'message' : warning_msgs
1151 return {'value': result, 'domain': domain, 'warning': warning}
1153 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1154 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1155 lang=False, update_tax=True, date_order=False, context=None):
1156 context = context or {}
1157 lang = lang or ('lang' in context and context['lang'])
1159 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1160 return self.product_id_change(cursor, user, ids, pricelist, product,
1161 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1162 partner_id=partner_id, lang=lang, update_tax=update_tax,
1163 date_order=date_order, context=context)
1165 def unlink(self, cr, uid, ids, context=None):
1168 """Allows to delete sales order lines in draft,cancel states"""
1169 for rec in self.browse(cr, uid, ids, context=context):
1170 if rec.state not in ['draft', 'cancel']:
1171 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1172 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1175 class mail_compose_message(osv.Model):
1176 _inherit = 'mail.compose.message'
1178 def send_mail(self, cr, uid, ids, context=None):
1179 context = context or {}
1180 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1181 context = dict(context, mail_post_autofollow=True)
1182 self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
1183 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1186 class account_invoice(osv.Model):
1187 _inherit = 'account.invoice'
1189 def _get_default_section_id(self, cr, uid, context=None):
1190 """ Gives default section by checking if present in the context """
1191 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1193 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1196 def _resolve_section_id_from_context(self, cr, uid, context=None):
1197 """ Returns ID of section based on the value of 'section_id'
1198 context key, or None if it cannot be resolved to a single
1203 if type(context.get('default_section_id')) in (int, long):
1204 return context.get('default_section_id')
1205 if isinstance(context.get('default_section_id'), basestring):
1206 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1207 if len(section_ids) == 1:
1208 return int(section_ids[0][0])
1212 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1216 'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1219 def confirm_paid(self, cr, uid, ids, context=None):
1220 sale_order_obj = self.pool.get('sale.order')
1221 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1222 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1223 for so_id in so_ids:
1224 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1227 def unlink(self, cr, uid, ids, context=None):
1228 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1229 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1230 #if we can't cancel all invoices, do nothing
1231 if len(invoice_ids) == len(ids):
1232 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1233 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1235 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1236 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1239 class procurement_order(osv.osv):
1240 _inherit = 'procurement.order'
1242 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1245 def write(self, cr, uid, ids, vals, context=None):
1246 if isinstance(ids, (int, long)):
1248 res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
1249 from openerp import workflow
1250 if vals.get('state') in ['done', 'cancel', 'exception']:
1251 for proc in self.browse(cr, uid, ids, context=context):
1252 if proc.sale_line_id and proc.sale_line_id.order_id:
1253 order_id = proc.sale_line_id.order_id.id
1254 if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
1255 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
1256 if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
1257 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
1260 class product_product(osv.Model):
1261 _inherit = 'product.product'
1263 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1264 SaleOrderLine = self.pool['sale.order.line']
1266 product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1267 for product_id in ids
1271 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1275 class product_template(osv.Model):
1276 _inherit = 'product.template'
1278 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1279 res = dict.fromkeys(ids, 0)
1280 for template in self.browse(cr, uid, ids, context=context):
1281 res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
1284 def action_view_sales(self, cr, uid, ids, context=None):
1285 act_obj = self.pool.get('ir.actions.act_window')
1286 mod_obj = self.pool.get('ir.model.data')
1288 for template in self.browse(cr, uid, ids, context=context):
1289 product_ids += [x.id for x in template.product_variant_ids]
1290 result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
1291 result = act_obj.read(cr, uid, [result], context=context)[0]
1292 result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
1297 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1301 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: