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
25 from openerp import pooler
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import openerp.addons.decimal_precision as dp
30 from openerp import netsvc
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 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40 'company_id': fields.many2one('res.company', 'Company', required=False),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 class sale_order(osv.osv):
50 _inherit = ['mail.thread', 'ir.needaction_mixin']
51 _description = "Sales Order"
54 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['manual', 'progress'],
55 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['sent']
59 def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
62 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
63 if shop.project_id.id:
64 v['project_id'] = shop.project_id.id
65 if shop.pricelist_id.id:
66 v['pricelist_id'] = shop.pricelist_id.id
69 def copy(self, cr, uid, id, default=None, context=None):
75 'date_confirm': False,
76 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
78 return super(sale_order, self).copy(cr, uid, id, default, context=context)
80 def _amount_line_tax(self, cr, uid, line, context=None):
82 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']:
83 val += c.get('amount', 0.0)
86 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
87 cur_obj = self.pool.get('res.currency')
89 for order in self.browse(cr, uid, ids, context=context):
91 'amount_untaxed': 0.0,
96 cur = order.pricelist_id.currency_id
97 for line in order.order_line:
98 val1 += line.price_subtotal
99 val += self._amount_line_tax(cr, uid, line, context=context)
100 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
101 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
102 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
106 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
108 for sale in self.browse(cursor, user, ids, context=context):
113 for invoice in sale.invoice_ids:
114 if invoice.state not in ('draft', 'cancel'):
115 tot += invoice.amount_untaxed
117 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
122 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
124 for sale in self.browse(cursor, user, ids, context=context):
130 def _invoiced(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
134 invoice_existence = False
135 for invoice in sale.invoice_ids:
136 if invoice.state!='cancel':
137 invoice_existence = True
138 if invoice.state != 'paid':
141 if not invoice_existence or sale.state == 'manual':
145 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
154 clause += 'AND inv.state = \'paid\''
156 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
157 sale_clause = ', sale_order AS sale '
160 cursor.execute('SELECT rel.order_id ' \
161 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
162 'WHERE rel.invoice_id = inv.id ' + clause)
163 res = cursor.fetchall()
165 cursor.execute('SELECT sale.id ' \
166 'FROM sale_order AS sale ' \
167 'WHERE sale.id NOT IN ' \
168 '(SELECT rel.order_id ' \
169 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
170 res.extend(cursor.fetchall())
172 return [('id', '=', 0)]
173 return [('id', 'in', [x[0] for x in res])]
175 def _get_order(self, cr, uid, ids, context=None):
177 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
178 result[line.order_id.id] = True
181 def _get_default_shop(self, cr, uid, context=None):
182 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
183 shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context)
185 raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!'))
189 'name': fields.char('Order Reference', size=64, required=True,
190 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
191 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
193 'client_order_ref': fields.char('Customer Reference', size=64),
194 'state': fields.selection([
195 ('draft', 'Draft Quotation'),
196 ('sent', 'Quotation Sent'),
197 ('cancel', 'Cancelled'),
198 ('waiting_date', 'Waiting Schedule'),
199 ('progress', 'Sales Order'),
200 ('manual', 'Sale to Invoice'),
201 ('invoice_except', 'Invoice Exception'),
203 ], 'Status', readonly=True, track_visibility='onchange',
204 help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
205 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
206 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
207 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
208 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
209 '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'),
210 '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."),
211 '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."),
212 'order_policy': fields.selection([
213 ('manual', 'On Demand'),
214 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
215 help="""This field controls how invoice and delivery operations are synchronized.
216 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
217 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
218 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
219 '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."),
221 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
222 '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)."),
223 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
224 'invoiced': fields.function(_invoiced, string='Paid',
225 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
226 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
227 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
228 'note': fields.text('Terms and conditions'),
230 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
232 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
233 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
235 multi='sums', help="The amount without tax.", track_visibility='always'),
236 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
238 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
239 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
241 multi='sums', help="The tax amount."),
242 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
244 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
245 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
247 multi='sums', help="The total amount."),
249 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
250 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
251 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
252 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
255 'date_order': fields.date.context_today,
256 'order_policy': 'manual',
258 'user_id': lambda obj, cr, uid, context: uid,
259 'name': lambda obj, cr, uid, context: '/',
260 'invoice_quantity': 'order',
261 'shop_id': _get_default_shop,
262 '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'],
263 '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'],
266 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
271 def unlink(self, cr, uid, ids, context=None):
272 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
274 for s in sale_orders:
275 if s['state'] in ['draft', 'cancel']:
276 unlink_ids.append(s['id'])
278 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before !'))
280 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
282 def copy_quotation(self, cr, uid, ids, context=None):
283 id = self.copy(cr, uid, ids[0], context=None)
284 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
285 view_id = view_ref and view_ref[1] or False,
287 'type': 'ir.actions.act_window',
288 'name': _('Sales Order'),
289 'res_model': 'sale.order',
298 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
299 context = context or {}
303 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
306 return {'value': value}
308 'title': _('Pricelist Warning!'),
309 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
311 return {'warning': warning, 'value': value}
313 def onchange_partner_id(self, cr, uid, ids, part, context=None):
315 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
317 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
318 #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the
319 #invoicing addresses and all the fields related to the partner.
320 if part.parent_id and not part.is_company:
321 part = part.parent_id
322 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
323 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
324 payment_term = part.property_payment_term and part.property_payment_term.id or False
325 fiscal_position = part.property_account_position and part.property_account_position.id or False
326 dedicated_salesman = part.user_id and part.user_id.id or uid
328 'partner_invoice_id': addr['invoice'],
329 'partner_shipping_id': addr['delivery'],
330 'payment_term': payment_term,
331 'fiscal_position': fiscal_position,
332 'user_id': dedicated_salesman,
335 val['pricelist_id'] = pricelist
336 return {'value': val}
338 def create(self, cr, uid, vals, context=None):
339 if vals.get('name','/')=='/':
340 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
341 return super(sale_order, self).create(cr, uid, vals, context=context)
343 def button_dummy(self, cr, uid, ids, context=None):
346 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
347 # can be removed after 6.1.
348 def _inv_get(self, cr, uid, order, context=None):
351 def _prepare_invoice(self, cr, uid, order, lines, context=None):
352 """Prepare the dict of values to create the new invoice for a
353 sales order. This method may be overridden to implement custom
354 invoice generation (making sure to call super() to establish
355 a clean extension chain).
357 :param browse_record order: sale.order record to invoice
358 :param list(int) line: list of invoice line IDs that must be
359 attached to the invoice
360 :return: dict of value to create() the invoice
364 journal_ids = self.pool.get('account.journal').search(cr, uid,
365 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
368 raise osv.except_osv(_('Error!'),
369 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
371 'name': order.client_order_ref or '',
372 'origin': order.name,
373 'type': 'out_invoice',
374 'reference': order.client_order_ref or order.name,
375 'account_id': order.partner_id.property_account_receivable.id,
376 'partner_id': order.partner_invoice_id.id,
377 'journal_id': journal_ids[0],
378 'invoice_line': [(6, 0, lines)],
379 'currency_id': order.pricelist_id.currency_id.id,
380 'comment': order.note,
381 'payment_term': order.payment_term and order.payment_term.id or False,
382 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
383 'date_invoice': context.get('date_invoice', False),
384 'company_id': order.company_id.id,
385 'user_id': order.user_id and order.user_id.id or False
388 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
389 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
392 def _make_invoice(self, cr, uid, order, lines, context=None):
393 inv_obj = self.pool.get('account.invoice')
394 obj_invoice_line = self.pool.get('account.invoice.line')
397 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
398 from_line_invoice_ids = []
399 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
400 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
401 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
402 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
403 for preinv in order.invoice_ids:
404 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
405 for preline in preinv.invoice_line:
406 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
407 lines.append(inv_line_id)
408 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
409 inv_id = inv_obj.create(cr, uid, inv, context=context)
410 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
411 if data.get('value', False):
412 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
413 inv_obj.button_compute(cr, uid, [inv_id])
416 def print_quotation(self, cr, uid, ids, context=None):
418 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
420 assert len(ids) == 1, 'This option should only be used for a single id at a time'
421 wf_service = netsvc.LocalService("workflow")
422 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
424 'model': 'sale.order',
426 'form': self.read(cr, uid, ids[0], context=context),
428 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
430 def manual_invoice(self, cr, uid, ids, context=None):
431 """ create invoices for the given sales orders (ids), and open the form
432 view of one of the newly created invoices
434 mod_obj = self.pool.get('ir.model.data')
435 wf_service = netsvc.LocalService("workflow")
437 # create invoices through the sales orders' workflow
438 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
440 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
441 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
442 # determine newly created invoices
443 new_inv_ids = list(inv_ids1 - inv_ids0)
445 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
446 res_id = res and res[1] or False,
449 'name': _('Customer Invoices'),
453 'res_model': 'account.invoice',
454 'context': "{'type':'out_invoice'}",
455 'type': 'ir.actions.act_window',
458 'res_id': new_inv_ids and new_inv_ids[0] or False,
461 def action_view_invoice(self, cr, uid, ids, context=None):
463 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.
465 mod_obj = self.pool.get('ir.model.data')
466 act_obj = self.pool.get('ir.actions.act_window')
468 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
469 id = result and result[1] or False
470 result = act_obj.read(cr, uid, [id], context=context)[0]
471 #compute the number of invoices to display
473 for so in self.browse(cr, uid, ids, context=context):
474 inv_ids += [invoice.id for invoice in so.invoice_ids]
475 #choose the view_mode accordingly
477 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
479 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
480 result['views'] = [(res and res[1] or False, 'form')]
481 result['res_id'] = inv_ids and inv_ids[0] or False
484 def test_no_product(self, cr, uid, order, context):
485 for line in order.order_line:
486 if line.product_id and (line.product_id.type<>'service'):
490 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
492 states = ['confirmed', 'done', 'exception']
496 invoice = self.pool.get('account.invoice')
497 obj_sale_order_line = self.pool.get('sale.order.line')
498 partner_currency = {}
501 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
502 # last day of the last month as invoice date
504 context['date_invoice'] = date_invoice
505 for o in self.browse(cr, uid, ids, context=context):
506 currency_id = o.pricelist_id.currency_id.id
507 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
508 raise osv.except_osv(
510 _('You cannot group sales having different currencies for the same partner.'))
512 partner_currency[o.partner_id.id] = currency_id
514 for line in o.order_line:
517 elif (line.state in states):
518 lines.append(line.id)
519 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
521 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
523 for o in self.browse(cr, uid, ids, context=context):
524 for i in o.invoice_ids:
525 if i.state == 'draft':
527 for val in invoices.values():
529 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
532 invoice_ref += o.name + '|'
533 self.write(cr, uid, [o.id], {'state': 'progress'})
534 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
535 #remove last '|' in invoice_ref
536 if len(invoice_ref) >= 1:
537 invoice_ref = invoice_ref[:-1]
538 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
540 for order, il in val:
541 res = self._make_invoice(cr, uid, order, il, context=context)
542 invoice_ids.append(res)
543 self.write(cr, uid, [order.id], {'state': 'progress'})
544 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
547 def action_invoice_cancel(self, cr, uid, ids, context=None):
548 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
551 def action_invoice_end(self, cr, uid, ids, context=None):
552 for this in self.browse(cr, uid, ids, context=context):
553 for line in this.order_line:
554 if line.state == 'exception':
555 line.write({'state': 'confirmed'})
556 if this.state == 'invoice_except':
557 this.write({'state': 'progress'})
560 def action_cancel(self, cr, uid, ids, context=None):
561 wf_service = netsvc.LocalService("workflow")
564 sale_order_line_obj = self.pool.get('sale.order.line')
565 for sale in self.browse(cr, uid, ids, context=context):
566 for inv in sale.invoice_ids:
567 if inv.state not in ('draft', 'cancel'):
568 raise osv.except_osv(
569 _('Cannot cancel this sales order!'),
570 _('First cancel all invoices attached to this sales order.'))
571 for r in self.read(cr, uid, ids, ['invoice_ids']):
572 for inv in r['invoice_ids']:
573 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
574 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
576 self.write(cr, uid, ids, {'state': 'cancel'})
579 def action_button_confirm(self, cr, uid, ids, context=None):
580 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
581 wf_service = netsvc.LocalService('workflow')
582 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
584 # redisplay the record as a sales order
585 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
586 view_id = view_ref and view_ref[1] or False,
588 'type': 'ir.actions.act_window',
589 'name': _('Sales Order'),
590 'res_model': 'sale.order',
599 def action_wait(self, cr, uid, ids, context=None):
600 context = context or {}
601 for o in self.browse(cr, uid, ids):
603 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
604 noprod = self.test_no_product(cr, uid, o, context)
605 if (o.order_policy == 'manual') or noprod:
606 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
608 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
609 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
612 def action_quotation_send(self, cr, uid, ids, context=None):
614 This function opens a window to compose an email, with the edi sale template message loaded by default
616 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
617 ir_model_data = self.pool.get('ir.model.data')
619 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
623 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
625 compose_form_id = False
628 'default_model': 'sale.order',
629 'default_res_id': ids[0],
630 'default_use_template': bool(template_id),
631 'default_template_id': template_id,
632 'default_composition_mode': 'comment',
633 'mark_so_as_sent': True
636 'type': 'ir.actions.act_window',
639 'res_model': 'mail.compose.message',
640 'views': [(compose_form_id, 'form')],
641 'view_id': compose_form_id,
646 def action_done(self, cr, uid, ids, context=None):
647 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
651 # TODO add a field price_unit_uos
652 # - update it on change product and unit price
653 # - use it in report if there is a uos
654 class sale_order_line(osv.osv):
656 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
657 tax_obj = self.pool.get('account.tax')
658 cur_obj = self.pool.get('res.currency')
662 for line in self.browse(cr, uid, ids, context=context):
663 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
664 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
665 cur = line.order_id.pricelist_id.currency_id
666 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
669 def _get_uom_id(self, cr, uid, *args):
671 proxy = self.pool.get('ir.model.data')
672 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
674 except Exception, ex:
677 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
678 res = dict.fromkeys(ids, False)
679 for this in self.browse(cr, uid, ids, context=context):
680 res[this.id] = this.invoice_lines and \
681 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
684 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
685 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
686 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
687 sale_order_line sol ON (sol.order_id = rel.order_id)
688 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
689 return [i[0] for i in cr.fetchall()]
691 _name = 'sale.order.line'
692 _description = 'Sales Order Line'
694 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
695 'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
696 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
697 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
698 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
699 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
701 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
702 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
703 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
704 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
705 help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."),
706 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
707 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
708 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
709 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
710 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
711 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
712 'product_uos': fields.many2one('product.uom', 'Product UoS'),
713 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
714 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
715 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
716 help='* The \'Draft\' status is set when the related sales order in draft status. \
717 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
718 \n* The \'Exception\' status is set when the related sales order is set as exception. \
719 \n* The \'Done\' status is set when the sales order line has been picked. \
720 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
721 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
722 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
723 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
725 _order = 'order_id desc, sequence, id'
727 'product_uom' : _get_uom_id,
729 'product_uom_qty': 1,
730 'product_uos_qty': 1,
733 'type': 'make_to_stock',
737 def _get_line_qty(self, cr, uid, line, context=None):
738 if (line.order_id.invoice_quantity=='order'):
740 return line.product_uos_qty or 0.0
741 return line.product_uom_qty
743 def _get_line_uom(self, cr, uid, line, context=None):
744 if (line.order_id.invoice_quantity=='order'):
746 return line.product_uos.id
747 return line.product_uom.id
749 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
750 """Prepare the dict of values to create the new invoice line for a
751 sales order line. This method may be overridden to implement custom
752 invoice generation (making sure to call super() to establish
753 a clean extension chain).
755 :param browse_record line: sale.order.line record to invoice
756 :param int account_id: optional ID of a G/L account to force
757 (this is used for returning products including service)
758 :return: dict of values to create() the invoice line
761 if not line.invoiced:
764 account_id = line.product_id.property_account_income.id
766 account_id = line.product_id.categ_id.property_account_income_categ.id
768 raise osv.except_osv(_('Error!'),
769 _('Please define income account for this product: "%s" (id:%d).') % \
770 (line.product_id.name, line.product_id.id,))
772 prop = self.pool.get('ir.property').get(cr, uid,
773 'property_account_income_categ', 'product.category',
775 account_id = prop and prop.id or False
776 uosqty = self._get_line_qty(cr, uid, line, context=context)
777 uos_id = self._get_line_uom(cr, uid, line, context=context)
780 pu = round(line.price_unit * line.product_uom_qty / uosqty,
781 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
782 fpos = line.order_id.fiscal_position or False
783 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
785 raise osv.except_osv(_('Error!'),
786 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
789 'sequence': line.sequence,
790 'origin': line.order_id.name,
791 'account_id': account_id,
794 'discount': line.discount,
796 'product_id': line.product_id.id or False,
797 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
798 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
803 def invoice_line_create(self, cr, uid, ids, context=None):
809 for line in self.browse(cr, uid, ids, context=context):
810 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
812 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
813 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
814 sales.add(line.order_id.id)
815 create_ids.append(inv_id)
816 # Trigger workflow events
817 wf_service = netsvc.LocalService("workflow")
818 for sale_id in sales:
819 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
822 def button_cancel(self, cr, uid, ids, context=None):
823 for line in self.browse(cr, uid, ids, context=context):
825 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
826 return self.write(cr, uid, ids, {'state': 'cancel'})
828 def button_confirm(self, cr, uid, ids, context=None):
829 return self.write(cr, uid, ids, {'state': 'confirmed'})
831 def button_done(self, cr, uid, ids, context=None):
832 wf_service = netsvc.LocalService("workflow")
833 res = self.write(cr, uid, ids, {'state': 'done'})
834 for line in self.browse(cr, uid, ids, context=context):
835 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
838 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
839 product_obj = self.pool.get('product.product')
841 return {'value': {'product_uom': product_uos,
842 'product_uom_qty': product_uos_qty}, 'domain': {}}
844 product = product_obj.browse(cr, uid, product_id)
846 'product_uom': product.uom_id.id,
848 # FIXME must depend on uos/uom of the product and not only of the coeff.
851 'product_uom_qty': product_uos_qty / product.uos_coeff,
852 'th_weight': product_uos_qty / product.uos_coeff * product.weight
854 except ZeroDivisionError:
856 return {'value': value}
858 def copy_data(self, cr, uid, id, default=None, context=None):
861 default.update({'state': 'draft', 'invoice_lines': []})
862 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
864 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
865 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
866 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
867 context = context or {}
868 lang = lang or context.get('lang',False)
870 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
872 product_uom_obj = self.pool.get('product.uom')
873 partner_obj = self.pool.get('res.partner')
874 product_obj = self.pool.get('product.product')
875 context = {'lang': lang, 'partner_id': partner_id}
877 lang = partner_obj.browse(cr, uid, partner_id).lang
878 context_partner = {'lang': lang, 'partner_id': partner_id}
881 return {'value': {'th_weight': 0,
882 'product_uos_qty': qty}, 'domain': {'product_uom': [],
885 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
889 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
893 uom2 = product_uom_obj.browse(cr, uid, uom)
894 if product_obj.uom_id.category_id.id != uom2.category_id.id:
897 if product_obj.uos_id:
898 uos2 = product_uom_obj.browse(cr, uid, uos)
899 if product_obj.uos_id.category_id.id != uos2.category_id.id:
903 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
904 if update_tax: #The quantity only have changed
905 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
908 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
909 if product_obj.description_sale:
910 result['name'] += '\n'+product_obj.description_sale
912 if (not uom) and (not uos):
913 result['product_uom'] = product_obj.uom_id.id
914 if product_obj.uos_id:
915 result['product_uos'] = product_obj.uos_id.id
916 result['product_uos_qty'] = qty * product_obj.uos_coeff
917 uos_category_id = product_obj.uos_id.category_id.id
919 result['product_uos'] = False
920 result['product_uos_qty'] = qty
921 uos_category_id = False
922 result['th_weight'] = qty * product_obj.weight
923 domain = {'product_uom':
924 [('category_id', '=', product_obj.uom_id.category_id.id)],
926 [('category_id', '=', uos_category_id)]}
927 elif uos and not uom: # only happens if uom is False
928 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
929 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
930 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
931 elif uom: # whether uos is set or not
932 default_uom = product_obj.uom_id and product_obj.uom_id.id
933 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
934 if product_obj.uos_id:
935 result['product_uos'] = product_obj.uos_id.id
936 result['product_uos_qty'] = qty * product_obj.uos_coeff
938 result['product_uos'] = False
939 result['product_uos_qty'] = qty
940 result['th_weight'] = q * product_obj.weight # Round the quantity up
943 uom2 = product_obj.uom_id
947 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
948 'Please set one before choosing a product.')
949 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
951 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
952 product, qty or 1.0, partner_id, {
953 'uom': uom or result.get('product_uom'),
957 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
958 "You have to change either the product, the quantity or the pricelist.")
960 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
962 result.update({'price_unit': price})
965 'title': _('Configuration Error!'),
966 'message' : warning_msgs
968 return {'value': result, 'domain': domain, 'warning': warning}
970 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
971 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
972 lang=False, update_tax=True, date_order=False, context=None):
973 context = context or {}
974 lang = lang or ('lang' in context and context['lang'])
976 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
977 return self.product_id_change(cursor, user, ids, pricelist, product,
978 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
979 partner_id=partner_id, lang=lang, update_tax=update_tax,
980 date_order=date_order, context=context)
982 def unlink(self, cr, uid, ids, context=None):
985 """Allows to delete sales order lines in draft,cancel states"""
986 for rec in self.browse(cr, uid, ids, context=context):
987 if rec.state not in ['draft', 'cancel']:
988 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
989 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
992 class mail_compose_message(osv.Model):
993 _inherit = 'mail.compose.message'
995 def send_mail(self, cr, uid, ids, context=None):
996 context = context or {}
997 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
998 context = dict(context, mail_post_autofollow=True)
999 wf_service = netsvc.LocalService("workflow")
1000 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1001 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1003 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: