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_quotation_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
182 'name': fields.char('Order Reference', size=64, required=True,
183 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
184 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
185 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
186 'client_order_ref': fields.char('Customer Reference', size=64),
187 'state': fields.selection([
188 ('draft', 'Draft Quotation'),
189 ('sent', 'Quotation Sent'),
190 ('cancel', 'Cancelled'),
191 ('waiting_date', 'Waiting Schedule'),
192 ('progress', 'Sale Order'),
193 ('manual', 'Sale to Invoice'),
194 ('invoice_except', 'Invoice Exception'),
196 ], 'Status', readonly=True, track_visibility=1,
197 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 sale 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),
198 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', 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."),
201 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility=1),
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=2),
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 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
210 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
211 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
212 '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."),
214 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
215 '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)."),
216 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
217 'invoiced': fields.function(_invoiced, string='Paid',
218 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
219 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
220 fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."),
221 'note': fields.text('Terms and conditions'),
223 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
225 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
226 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
228 multi='sums', help="The amount without tax.", track_visibility=2),
229 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
231 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
232 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234 multi='sums', help="The tax amount."),
235 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240 multi='sums', help="The total amount."),
242 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
243 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
244 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
245 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
248 'date_order': fields.date.context_today,
249 'order_policy': 'manual',
251 'user_id': lambda obj, cr, uid, context: uid,
252 'name': lambda obj, cr, uid, context: '/',
253 'invoice_quantity': 'order',
254 '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'],
255 '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 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
263 def unlink(self, cr, uid, ids, context=None):
264 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
266 for s in sale_orders:
267 if s['state'] in ['draft', 'cancel']:
268 unlink_ids.append(s['id'])
270 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
272 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
274 def copy_quotation(self, cr, uid, ids, context=None):
275 id = self.copy(cr, uid, ids[0], context=None)
276 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
277 view_id = view_ref and view_ref[1] or False,
279 'type': 'ir.actions.act_window',
280 'name': _('Sales Order'),
281 'res_model': 'sale.order',
290 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
291 context = context or {}
295 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
298 return {'value': value}
300 'title': _('Pricelist Warning!'),
301 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
303 return {'warning': warning, 'value': value}
305 def onchange_partner_id(self, cr, uid, ids, part, context=None):
307 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
309 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
310 #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the
311 #invoicing addresses and all the fields related to the partner.
312 if part.parent_id and not part.is_company:
313 part = part.parent_id
314 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
315 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
316 payment_term = part.property_payment_term and part.property_payment_term.id or False
317 fiscal_position = part.property_account_position and part.property_account_position.id or False
318 dedicated_salesman = part.user_id and part.user_id.id or uid
320 'partner_invoice_id': addr['invoice'],
321 'partner_shipping_id': addr['delivery'],
322 'payment_term': payment_term,
323 'fiscal_position': fiscal_position,
324 'user_id': dedicated_salesman,
327 val['pricelist_id'] = pricelist
328 return {'value': val}
330 def create(self, cr, uid, vals, context=None):
331 if vals.get('name','/')=='/':
332 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
333 return super(sale_order, self).create(cr, uid, vals, context=context)
335 def button_dummy(self, cr, uid, ids, context=None):
338 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
339 # can be removed after 6.1.
340 def _inv_get(self, cr, uid, order, context=None):
343 def _prepare_invoice(self, cr, uid, order, lines, context=None):
344 """Prepare the dict of values to create the new invoice for a
345 sale order. This method may be overridden to implement custom
346 invoice generation (making sure to call super() to establish
347 a clean extension chain).
349 :param browse_record order: sale.order record to invoice
350 :param list(int) line: list of invoice line IDs that must be
351 attached to the invoice
352 :return: dict of value to create() the invoice
356 journal_ids = self.pool.get('account.journal').search(cr, uid,
357 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
360 raise osv.except_osv(_('Error!'),
361 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
363 'name': order.client_order_ref or '',
364 'origin': order.name,
365 'type': 'out_invoice',
366 'reference': order.client_order_ref or order.name,
367 'account_id': order.partner_id.property_account_receivable.id,
368 'partner_id': order.partner_invoice_id.id,
369 'journal_id': journal_ids[0],
370 'invoice_line': [(6, 0, lines)],
371 'currency_id': order.pricelist_id.currency_id.id,
372 'comment': order.note,
373 'payment_term': order.payment_term and order.payment_term.id or False,
374 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
375 'date_invoice': context.get('date_invoice', False),
376 'company_id': order.company_id.id,
377 'user_id': order.user_id and order.user_id.id or False
380 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
381 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
384 def _make_invoice(self, cr, uid, order, lines, context=None):
385 inv_obj = self.pool.get('account.invoice')
386 obj_invoice_line = self.pool.get('account.invoice.line')
389 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
390 from_line_invoice_ids = []
391 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
392 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
393 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
394 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
395 for preinv in order.invoice_ids:
396 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
397 for preline in preinv.invoice_line:
398 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
399 lines.append(inv_line_id)
400 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
401 inv_id = inv_obj.create(cr, uid, inv, context=context)
402 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
403 if data.get('value', False):
404 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
405 inv_obj.button_compute(cr, uid, [inv_id])
408 def print_quotation(self, cr, uid, ids, context=None):
410 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
412 assert len(ids) == 1, 'This option should only be used for a single id at a time'
413 wf_service = netsvc.LocalService("workflow")
414 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
416 'model': 'sale.order',
418 'form': self.read(cr, uid, ids[0], context=context),
420 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
422 def manual_invoice(self, cr, uid, ids, context=None):
423 """ create invoices for the given sale orders (ids), and open the form
424 view of one of the newly created invoices
426 mod_obj = self.pool.get('ir.model.data')
427 wf_service = netsvc.LocalService("workflow")
429 # create invoices through the sale orders' workflow
430 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
432 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
433 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
434 # determine newly created invoices
435 new_inv_ids = list(inv_ids1 - inv_ids0)
437 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
438 res_id = res and res[1] or False,
441 'name': _('Customer Invoices'),
445 'res_model': 'account.invoice',
446 'context': "{'type':'out_invoice'}",
447 'type': 'ir.actions.act_window',
450 'res_id': new_inv_ids and new_inv_ids[0] or False,
453 def action_view_invoice(self, cr, uid, ids, context=None):
455 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
457 mod_obj = self.pool.get('ir.model.data')
458 act_obj = self.pool.get('ir.actions.act_window')
460 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
461 id = result and result[1] or False
462 result = act_obj.read(cr, uid, [id], context=context)[0]
463 #compute the number of invoices to display
465 for so in self.browse(cr, uid, ids, context=context):
466 inv_ids += [invoice.id for invoice in so.invoice_ids]
467 #choose the view_mode accordingly
469 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
471 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
472 result['views'] = [(res and res[1] or False, 'form')]
473 result['res_id'] = inv_ids and inv_ids[0] or False
476 def test_no_product(self, cr, uid, order, context):
477 for line in order.order_line:
478 if line.product_id and (line.product_id.type<>'service'):
482 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
484 states = ['confirmed', 'done', 'exception']
488 invoice = self.pool.get('account.invoice')
489 obj_sale_order_line = self.pool.get('sale.order.line')
490 partner_currency = {}
493 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
494 # last day of the last month as invoice date
496 context['date_invoice'] = date_invoice
497 for o in self.browse(cr, uid, ids, context=context):
498 currency_id = o.pricelist_id.currency_id.id
499 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
500 raise osv.except_osv(
502 _('You cannot group sales having different currencies for the same partner.'))
504 partner_currency[o.partner_id.id] = currency_id
506 for line in o.order_line:
509 elif (line.state in states):
510 lines.append(line.id)
511 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
513 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
515 for o in self.browse(cr, uid, ids, context=context):
516 for i in o.invoice_ids:
517 if i.state == 'draft':
519 for val in invoices.values():
521 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
524 invoice_ref += o.name + '|'
525 self.write(cr, uid, [o.id], {'state': 'progress'})
526 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
527 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
529 for order, il in val:
530 res = self._make_invoice(cr, uid, order, il, context=context)
531 invoice_ids.append(res)
532 self.write(cr, uid, [order.id], {'state': 'progress'})
533 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
536 def action_invoice_cancel(self, cr, uid, ids, context=None):
537 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
540 def action_invoice_end(self, cr, uid, ids, context=None):
541 for this in self.browse(cr, uid, ids, context=context):
542 for line in this.order_line:
543 if line.state == 'exception':
544 line.write({'state': 'confirmed'})
545 if this.state == 'invoice_except':
546 this.write({'state': 'progress'})
549 def action_cancel(self, cr, uid, ids, context=None):
550 wf_service = netsvc.LocalService("workflow")
553 sale_order_line_obj = self.pool.get('sale.order.line')
554 for sale in self.browse(cr, uid, ids, context=context):
555 for inv in sale.invoice_ids:
556 if inv.state not in ('draft', 'cancel'):
557 raise osv.except_osv(
558 _('Cannot cancel this sales order!'),
559 _('First cancel all invoices attached to this sales order.'))
560 for r in self.read(cr, uid, ids, ['invoice_ids']):
561 for inv in r['invoice_ids']:
562 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
563 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
565 self.write(cr, uid, ids, {'state': 'cancel'})
568 def action_button_confirm(self, cr, uid, ids, context=None):
569 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
570 wf_service = netsvc.LocalService('workflow')
571 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
573 # redisplay the record as a sale order
574 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
575 view_id = view_ref and view_ref[1] or False,
577 'type': 'ir.actions.act_window',
578 'name': _('Sales Order'),
579 'res_model': 'sale.order',
588 def action_wait(self, cr, uid, ids, context=None):
589 context = context or {}
590 for o in self.browse(cr, uid, ids):
592 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
593 noprod = self.test_no_product(cr, uid, o, context)
594 if (o.order_policy == 'manual') or noprod:
595 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
597 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
598 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
601 def action_quotation_send(self, cr, uid, ids, context=None):
603 This function opens a window to compose an email, with the edi sale template message loaded by default
605 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
606 ir_model_data = self.pool.get('ir.model.data')
608 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
612 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
614 compose_form_id = False
617 'default_model': 'sale.order',
618 'default_res_id': ids[0],
619 'default_use_template': bool(template_id),
620 'default_template_id': template_id,
621 'default_composition_mode': 'comment',
622 'mark_so_as_sent': True
625 'type': 'ir.actions.act_window',
628 'res_model': 'mail.compose.message',
629 'views': [(compose_form_id, 'form')],
630 'view_id': compose_form_id,
635 def action_done(self, cr, uid, ids, context=None):
636 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
638 # ------------------------------------------------
639 # OpenChatter methods and notifications
640 # ------------------------------------------------
642 def needaction_domain_get(self, cr, uid, ids, context=None):
643 return [('state', '=', 'draft'), ('user_id', '=', uid)]
646 # TODO add a field price_unit_uos
647 # - update it on change product and unit price
648 # - use it in report if there is a uos
649 class sale_order_line(osv.osv):
651 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
652 tax_obj = self.pool.get('account.tax')
653 cur_obj = self.pool.get('res.currency')
657 for line in self.browse(cr, uid, ids, context=context):
658 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
659 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
660 cur = line.order_id.pricelist_id.currency_id
661 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
664 def _get_uom_id(self, cr, uid, *args):
666 proxy = self.pool.get('ir.model.data')
667 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
669 except Exception, ex:
672 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
673 res = dict.fromkeys(ids, False)
674 for this in self.browse(cr, uid, ids, context=context):
675 res[this.id] = this.invoice_lines and \
676 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
679 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
680 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
681 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
682 sale_order_line sol ON (sol.order_id = rel.order_id)
683 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
684 return [i[0] for i in cr.fetchall()]
686 _name = 'sale.order.line'
687 _description = 'Sales Order Line'
689 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
690 'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
691 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
692 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
693 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
694 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
695 store={'account.invoice': (_order_lines_from_invoice, ['state'], 10)}),
696 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
697 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
698 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."),
699 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
700 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
701 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
702 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
703 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
704 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
705 'product_uos': fields.many2one('product.uom', 'Product UoS'),
706 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
707 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
708 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
709 help='* The \'Draft\' status is set when the related sales order in draft status. \
710 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
711 \n* The \'Exception\' status is set when the related sales order is set as exception. \
712 \n* The \'Done\' status is set when the sales order line has been picked. \
713 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
714 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
715 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
716 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
718 _order = 'order_id desc, sequence'
720 'product_uom' : _get_uom_id,
722 'product_uom_qty': 1,
723 'product_uos_qty': 1,
726 'type': 'make_to_stock',
730 def _get_line_qty(self, cr, uid, line, context=None):
731 if (line.order_id.invoice_quantity=='order'):
733 return line.product_uos_qty or 0.0
734 return line.product_uom_qty
736 def _get_line_uom(self, cr, uid, line, context=None):
737 if (line.order_id.invoice_quantity=='order'):
739 return line.product_uos.id
740 return line.product_uom.id
742 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
743 """Prepare the dict of values to create the new invoice line for a
744 sale order line. This method may be overridden to implement custom
745 invoice generation (making sure to call super() to establish
746 a clean extension chain).
748 :param browse_record line: sale.order.line record to invoice
749 :param int account_id: optional ID of a G/L account to force
750 (this is used for returning products including service)
751 :return: dict of values to create() the invoice line
754 if not line.invoiced:
757 account_id = line.product_id.property_account_income.id
759 account_id = line.product_id.categ_id.property_account_income_categ.id
761 raise osv.except_osv(_('Error!'),
762 _('Please define income account for this product: "%s" (id:%d).') % \
763 (line.product_id.name, line.product_id.id,))
765 prop = self.pool.get('ir.property').get(cr, uid,
766 'property_account_income_categ', 'product.category',
768 account_id = prop and prop.id or False
769 uosqty = self._get_line_qty(cr, uid, line, context=context)
770 uos_id = self._get_line_uom(cr, uid, line, context=context)
773 pu = round(line.price_unit * line.product_uom_qty / uosqty,
774 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
775 fpos = line.order_id.fiscal_position or False
776 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
778 raise osv.except_osv(_('Error!'),
779 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
782 'sequence': line.sequence,
783 'origin': line.order_id.name,
784 'account_id': account_id,
787 'discount': line.discount,
789 'product_id': line.product_id.id or False,
790 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
791 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
796 def invoice_line_create(self, cr, uid, ids, context=None):
802 for line in self.browse(cr, uid, ids, context=context):
803 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
805 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
806 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
807 sales.add(line.order_id.id)
808 create_ids.append(inv_id)
809 # Trigger workflow events
810 wf_service = netsvc.LocalService("workflow")
811 for sale_id in sales:
812 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
815 def button_cancel(self, cr, uid, ids, context=None):
816 for line in self.browse(cr, uid, ids, context=context):
818 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
819 return self.write(cr, uid, ids, {'state': 'cancel'})
821 def button_confirm(self, cr, uid, ids, context=None):
822 return self.write(cr, uid, ids, {'state': 'confirmed'})
824 def button_done(self, cr, uid, ids, context=None):
825 wf_service = netsvc.LocalService("workflow")
826 res = self.write(cr, uid, ids, {'state': 'done'})
827 for line in self.browse(cr, uid, ids, context=context):
828 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
831 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
832 product_obj = self.pool.get('product.product')
834 return {'value': {'product_uom': product_uos,
835 'product_uom_qty': product_uos_qty}, 'domain': {}}
837 product = product_obj.browse(cr, uid, product_id)
839 'product_uom': product.uom_id.id,
841 # FIXME must depend on uos/uom of the product and not only of the coeff.
844 'product_uom_qty': product_uos_qty / product.uos_coeff,
845 'th_weight': product_uos_qty / product.uos_coeff * product.weight
847 except ZeroDivisionError:
849 return {'value': value}
851 def copy_data(self, cr, uid, id, default=None, context=None):
854 default.update({'state': 'draft', 'invoice_lines': []})
855 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
857 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
858 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
859 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
860 context = context or {}
861 lang = lang or context.get('lang',False)
863 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
865 product_uom_obj = self.pool.get('product.uom')
866 partner_obj = self.pool.get('res.partner')
867 product_obj = self.pool.get('product.product')
868 context = {'lang': lang, 'partner_id': partner_id}
870 lang = partner_obj.browse(cr, uid, partner_id).lang
871 context_partner = {'lang': lang, 'partner_id': partner_id}
874 return {'value': {'th_weight': 0,
875 'product_uos_qty': qty}, 'domain': {'product_uom': [],
878 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
882 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
886 uom2 = product_uom_obj.browse(cr, uid, uom)
887 if product_obj.uom_id.category_id.id != uom2.category_id.id:
890 if product_obj.uos_id:
891 uos2 = product_uom_obj.browse(cr, uid, uos)
892 if product_obj.uos_id.category_id.id != uos2.category_id.id:
896 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
897 if update_tax: #The quantity only have changed
898 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
901 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
902 if product_obj.description_sale:
903 result['name'] += '\n'+product_obj.description_sale
905 if (not uom) and (not uos):
906 result['product_uom'] = product_obj.uom_id.id
907 if product_obj.uos_id:
908 result['product_uos'] = product_obj.uos_id.id
909 result['product_uos_qty'] = qty * product_obj.uos_coeff
910 uos_category_id = product_obj.uos_id.category_id.id
912 result['product_uos'] = False
913 result['product_uos_qty'] = qty
914 uos_category_id = False
915 result['th_weight'] = qty * product_obj.weight
916 domain = {'product_uom':
917 [('category_id', '=', product_obj.uom_id.category_id.id)],
919 [('category_id', '=', uos_category_id)]}
920 elif uos and not uom: # only happens if uom is False
921 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
922 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
923 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
924 elif uom: # whether uos is set or not
925 default_uom = product_obj.uom_id and product_obj.uom_id.id
926 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
927 result['product_uom'] = default_uom
928 if product_obj.uos_id:
929 result['product_uos'] = product_obj.uos_id.id
930 result['product_uos_qty'] = qty * product_obj.uos_coeff
932 result['product_uos'] = False
933 result['product_uos_qty'] = qty
934 result['th_weight'] = q * product_obj.weight # Round the quantity up
937 uom2 = product_obj.uom_id
941 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
942 'Please set one before choosing a product.')
943 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
945 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
946 product, qty or 1.0, partner_id, {
947 'uom': uom or result.get('product_uom'),
951 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
952 "You have to change either the product, the quantity or the pricelist.")
954 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
956 result.update({'price_unit': price})
959 'title': _('Configuration Error!'),
960 'message' : warning_msgs
962 return {'value': result, 'domain': domain, 'warning': warning}
964 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
965 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
966 lang=False, update_tax=True, date_order=False, context=None):
967 context = context or {}
968 lang = lang or ('lang' in context and context['lang'])
970 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
971 return self.product_id_change(cursor, user, ids, pricelist, product,
972 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
973 partner_id=partner_id, lang=lang, update_tax=update_tax,
974 date_order=date_order, context=context)
976 def unlink(self, cr, uid, ids, context=None):
979 """Allows to delete sales order lines in draft,cancel states"""
980 for rec in self.browse(cr, uid, ids, context=context):
981 if rec.state not in ['draft', 'cancel']:
982 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
983 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
986 class mail_compose_message(osv.osv):
987 _inherit = 'mail.compose.message'
988 def send_mail(self, cr, uid, ids, context=None):
989 context = context or {}
990 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
991 wf_service = netsvc.LocalService("workflow")
992 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
993 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
995 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: