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):
73 'date_order': fields.date.context_today(self, cr, uid, context=context),
76 'date_confirm': False,
77 'client_order_ref': '',
78 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
80 return super(sale_order, self).copy(cr, uid, id, default, context=context)
82 def _amount_line_tax(self, cr, uid, line, context=None):
84 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']:
85 val += c.get('amount', 0.0)
88 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
89 cur_obj = self.pool.get('res.currency')
91 for order in self.browse(cr, uid, ids, context=context):
93 'amount_untaxed': 0.0,
98 cur = order.pricelist_id.currency_id
99 for line in order.order_line:
100 val1 += line.price_subtotal
101 val += self._amount_line_tax(cr, uid, line, context=context)
102 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
103 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
104 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
108 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
110 for sale in self.browse(cursor, user, ids, context=context):
115 for invoice in sale.invoice_ids:
116 if invoice.state not in ('draft', 'cancel'):
117 tot += invoice.amount_untaxed
119 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
124 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
132 def _invoiced(self, cursor, user, ids, name, arg, context=None):
134 for sale in self.browse(cursor, user, ids, context=context):
136 invoice_existence = False
137 for invoice in sale.invoice_ids:
138 if invoice.state!='cancel':
139 invoice_existence = True
140 if invoice.state != 'paid':
143 if not invoice_existence or sale.state == 'manual':
147 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
156 clause += 'AND inv.state = \'paid\''
158 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
159 sale_clause = ', sale_order AS sale '
162 cursor.execute('SELECT rel.order_id ' \
163 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
164 'WHERE rel.invoice_id = inv.id ' + clause)
165 res = cursor.fetchall()
167 cursor.execute('SELECT sale.id ' \
168 'FROM sale_order AS sale ' \
169 'WHERE sale.id NOT IN ' \
170 '(SELECT rel.order_id ' \
171 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
172 res.extend(cursor.fetchall())
174 return [('id', '=', 0)]
175 return [('id', 'in', [x[0] for x in res])]
177 def _get_order(self, cr, uid, ids, context=None):
179 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
180 result[line.order_id.id] = True
183 def _get_default_shop(self, cr, uid, context=None):
184 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
185 shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context)
187 raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!'))
191 'name': fields.char('Order Reference', size=64, required=True,
192 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
193 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
194 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
195 'client_order_ref': fields.char('Customer Reference', size=64),
196 'state': fields.selection([
197 ('draft', 'Draft Quotation'),
198 ('sent', 'Quotation Sent'),
199 ('cancel', 'Cancelled'),
200 ('waiting_date', 'Waiting Schedule'),
201 ('progress', 'Sales Order'),
202 ('manual', 'Sale to Invoice'),
203 ('invoice_except', 'Invoice Exception'),
205 ], 'Status', readonly=True, track_visibility='onchange',
206 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),
207 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
208 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
209 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
210 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
211 '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'),
212 '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."),
213 '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."),
214 'order_policy': fields.selection([
215 ('manual', 'On Demand'),
216 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
217 help="""This field controls how invoice and delivery operations are synchronized."""),
218 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
219 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
220 '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."),
222 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
223 '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)."),
224 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
225 'invoiced': fields.function(_invoiced, string='Paid',
226 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
227 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
228 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
229 'note': fields.text('Terms and conditions'),
231 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
233 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
234 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
236 multi='sums', help="The amount without tax.", track_visibility='always'),
237 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
239 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
240 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
242 multi='sums', help="The tax amount."),
243 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
245 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
246 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
248 multi='sums', help="The total amount."),
250 '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)]}),
251 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
252 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
253 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
256 'date_order': fields.date.context_today,
257 'order_policy': 'manual',
259 'user_id': lambda obj, cr, uid, context: uid,
260 'name': lambda obj, cr, uid, context: '/',
261 'invoice_quantity': 'order',
262 'shop_id': _get_default_shop,
263 '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'],
264 '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'],
267 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
272 def unlink(self, cr, uid, ids, context=None):
273 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
275 for s in sale_orders:
276 if s['state'] in ['draft', 'cancel']:
277 unlink_ids.append(s['id'])
279 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
281 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
283 def copy_quotation(self, cr, uid, ids, context=None):
284 id = self.copy(cr, uid, ids[0], context=None)
285 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
286 view_id = view_ref and view_ref[1] or False,
288 'type': 'ir.actions.act_window',
289 'name': _('Sales Order'),
290 'res_model': 'sale.order',
299 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
300 context = context or {}
304 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
307 return {'value': value}
309 'title': _('Pricelist Warning!'),
310 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
312 return {'warning': warning, 'value': value}
314 def onchange_partner_id(self, cr, uid, ids, part, context=None):
316 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
318 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
319 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
320 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
321 payment_term = part.property_payment_term and part.property_payment_term.id or False
322 fiscal_position = part.property_account_position and part.property_account_position.id or False
323 dedicated_salesman = part.user_id and part.user_id.id or uid
325 'partner_invoice_id': addr['invoice'],
326 'partner_shipping_id': addr['delivery'],
327 'payment_term': payment_term,
328 'fiscal_position': fiscal_position,
329 'user_id': dedicated_salesman,
332 val['pricelist_id'] = pricelist
333 return {'value': val}
335 def create(self, cr, uid, vals, context=None):
336 if vals.get('name','/')=='/':
337 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
338 return super(sale_order, self).create(cr, uid, vals, context=context)
340 def button_dummy(self, cr, uid, ids, context=None):
343 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
344 # can be removed after 6.1.
345 def _inv_get(self, cr, uid, order, context=None):
348 def _prepare_invoice(self, cr, uid, order, lines, context=None):
349 """Prepare the dict of values to create the new invoice for a
350 sales order. This method may be overridden to implement custom
351 invoice generation (making sure to call super() to establish
352 a clean extension chain).
354 :param browse_record order: sale.order record to invoice
355 :param list(int) line: list of invoice line IDs that must be
356 attached to the invoice
357 :return: dict of value to create() the invoice
361 journal_ids = self.pool.get('account.journal').search(cr, uid,
362 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
365 raise osv.except_osv(_('Error!'),
366 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
368 'name': order.client_order_ref or '',
369 'origin': order.name,
370 'type': 'out_invoice',
371 'reference': order.client_order_ref or order.name,
372 'account_id': order.partner_id.property_account_receivable.id,
373 'partner_id': order.partner_invoice_id.id,
374 'journal_id': journal_ids[0],
375 'invoice_line': [(6, 0, lines)],
376 'currency_id': order.pricelist_id.currency_id.id,
377 'comment': order.note,
378 'payment_term': order.payment_term and order.payment_term.id or False,
379 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
380 'date_invoice': context.get('date_invoice', False),
381 'company_id': order.company_id.id,
382 'user_id': order.user_id and order.user_id.id or False
385 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
386 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
389 def _make_invoice(self, cr, uid, order, lines, context=None):
390 inv_obj = self.pool.get('account.invoice')
391 obj_invoice_line = self.pool.get('account.invoice.line')
394 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
395 from_line_invoice_ids = []
396 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
397 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
398 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
399 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
400 for preinv in order.invoice_ids:
401 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
402 for preline in preinv.invoice_line:
403 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
404 lines.append(inv_line_id)
405 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
406 inv_id = inv_obj.create(cr, uid, inv, context=context)
407 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
408 if data.get('value', False):
409 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
410 inv_obj.button_compute(cr, uid, [inv_id])
413 def print_quotation(self, cr, uid, ids, context=None):
415 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
417 assert len(ids) == 1, 'This option should only be used for a single id at a time'
418 wf_service = netsvc.LocalService("workflow")
419 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
421 'model': 'sale.order',
423 'form': self.read(cr, uid, ids[0], context=context),
425 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
427 def manual_invoice(self, cr, uid, ids, context=None):
428 """ create invoices for the given sales orders (ids), and open the form
429 view of one of the newly created invoices
431 mod_obj = self.pool.get('ir.model.data')
432 wf_service = netsvc.LocalService("workflow")
434 # create invoices through the sales orders' workflow
435 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
437 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
438 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
439 # determine newly created invoices
440 new_inv_ids = list(inv_ids1 - inv_ids0)
442 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
443 res_id = res and res[1] or False,
446 'name': _('Customer Invoices'),
450 'res_model': 'account.invoice',
451 'context': "{'type':'out_invoice'}",
452 'type': 'ir.actions.act_window',
455 'res_id': new_inv_ids and new_inv_ids[0] or False,
458 def action_view_invoice(self, cr, uid, ids, context=None):
460 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.
462 mod_obj = self.pool.get('ir.model.data')
463 act_obj = self.pool.get('ir.actions.act_window')
465 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
466 id = result and result[1] or False
467 result = act_obj.read(cr, uid, [id], context=context)[0]
468 #compute the number of invoices to display
470 for so in self.browse(cr, uid, ids, context=context):
471 inv_ids += [invoice.id for invoice in so.invoice_ids]
472 #choose the view_mode accordingly
474 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
476 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
477 result['views'] = [(res and res[1] or False, 'form')]
478 result['res_id'] = inv_ids and inv_ids[0] or False
481 def test_no_product(self, cr, uid, order, context):
482 for line in order.order_line:
483 if line.product_id and (line.product_id.type<>'service'):
487 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
489 states = ['confirmed', 'done', 'exception']
493 invoice = self.pool.get('account.invoice')
494 obj_sale_order_line = self.pool.get('sale.order.line')
495 partner_currency = {}
498 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
499 # last day of the last month as invoice date
501 context['date_invoice'] = date_invoice
502 for o in self.browse(cr, uid, ids, context=context):
503 currency_id = o.pricelist_id.currency_id.id
504 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
505 raise osv.except_osv(
507 _('You cannot group sales having different currencies for the same partner.'))
509 partner_currency[o.partner_id.id] = currency_id
511 for line in o.order_line:
514 elif (line.state in states):
515 lines.append(line.id)
516 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
518 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
520 for o in self.browse(cr, uid, ids, context=context):
521 for i in o.invoice_ids:
522 if i.state == 'draft':
524 for val in invoices.values():
526 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
529 invoice_ref += o.name + '|'
530 self.write(cr, uid, [o.id], {'state': 'progress'})
531 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
532 #remove last '|' in invoice_ref
533 if len(invoice_ref) >= 1:
534 invoice_ref = invoice_ref[:-1]
535 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
537 for order, il in val:
538 res = self._make_invoice(cr, uid, order, il, context=context)
539 invoice_ids.append(res)
540 self.write(cr, uid, [order.id], {'state': 'progress'})
541 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
544 def action_invoice_cancel(self, cr, uid, ids, context=None):
545 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
548 def action_invoice_end(self, cr, uid, ids, context=None):
549 for this in self.browse(cr, uid, ids, context=context):
550 for line in this.order_line:
551 if line.state == 'exception':
552 line.write({'state': 'confirmed'})
553 if this.state == 'invoice_except':
554 this.write({'state': 'progress'})
557 def action_cancel(self, cr, uid, ids, context=None):
558 wf_service = netsvc.LocalService("workflow")
561 sale_order_line_obj = self.pool.get('sale.order.line')
562 for sale in self.browse(cr, uid, ids, context=context):
563 for inv in sale.invoice_ids:
564 if inv.state not in ('draft', 'cancel'):
565 raise osv.except_osv(
566 _('Cannot cancel this sales order!'),
567 _('First cancel all invoices attached to this sales order.'))
568 for r in self.read(cr, uid, ids, ['invoice_ids']):
569 for inv in r['invoice_ids']:
570 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
571 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
573 self.write(cr, uid, ids, {'state': 'cancel'})
576 def action_button_confirm(self, cr, uid, ids, context=None):
577 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
578 wf_service = netsvc.LocalService('workflow')
579 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
581 # redisplay the record as a sales order
582 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
583 view_id = view_ref and view_ref[1] or False,
585 'type': 'ir.actions.act_window',
586 'name': _('Sales Order'),
587 'res_model': 'sale.order',
596 def action_wait(self, cr, uid, ids, context=None):
597 context = context or {}
598 for o in self.browse(cr, uid, ids):
600 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
601 noprod = self.test_no_product(cr, uid, o, context)
602 if (o.order_policy == 'manual') or noprod:
603 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
605 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
606 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
609 def action_quotation_send(self, cr, uid, ids, context=None):
611 This function opens a window to compose an email, with the edi sale template message loaded by default
613 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
614 ir_model_data = self.pool.get('ir.model.data')
616 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
620 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
622 compose_form_id = False
625 'default_model': 'sale.order',
626 'default_res_id': ids[0],
627 'default_use_template': bool(template_id),
628 'default_template_id': template_id,
629 'default_composition_mode': 'comment',
630 'mark_so_as_sent': True
633 'type': 'ir.actions.act_window',
636 'res_model': 'mail.compose.message',
637 'views': [(compose_form_id, 'form')],
638 'view_id': compose_form_id,
643 def action_done(self, cr, uid, ids, context=None):
644 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
648 # TODO add a field price_unit_uos
649 # - update it on change product and unit price
650 # - use it in report if there is a uos
651 class sale_order_line(osv.osv):
653 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
654 tax_obj = self.pool.get('account.tax')
655 cur_obj = self.pool.get('res.currency')
659 for line in self.browse(cr, uid, ids, context=context):
660 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
661 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
662 cur = line.order_id.pricelist_id.currency_id
663 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
666 def _get_uom_id(self, cr, uid, *args):
668 proxy = self.pool.get('ir.model.data')
669 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
671 except Exception, ex:
674 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
675 res = dict.fromkeys(ids, False)
676 for this in self.browse(cr, uid, ids, context=context):
677 res[this.id] = this.invoice_lines and \
678 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
681 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
682 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
683 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
684 sale_order_line sol ON (sol.order_id = rel.order_id)
685 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
686 return [i[0] for i in cr.fetchall()]
688 _name = 'sale.order.line'
689 _description = 'Sales Order Line'
691 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
692 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
693 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
694 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
695 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
696 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
698 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
699 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
700 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
701 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
702 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."),
703 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
704 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
705 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
706 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
707 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
708 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
709 'product_uos': fields.many2one('product.uom', 'Product UoS'),
710 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
711 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
712 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
713 help='* The \'Draft\' status is set when the related sales order in draft status. \
714 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
715 \n* The \'Exception\' status is set when the related sales order is set as exception. \
716 \n* The \'Done\' status is set when the sales order line has been picked. \
717 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
718 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
719 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
720 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
722 _order = 'order_id desc, sequence, id'
724 'product_uom' : _get_uom_id,
726 'product_uom_qty': 1,
727 'product_uos_qty': 1,
730 'type': 'make_to_stock',
734 def _get_line_qty(self, cr, uid, line, context=None):
735 if (line.order_id.invoice_quantity=='order'):
737 return line.product_uos_qty or 0.0
738 return line.product_uom_qty
740 def _get_line_uom(self, cr, uid, line, context=None):
741 if (line.order_id.invoice_quantity=='order'):
743 return line.product_uos.id
744 return line.product_uom.id
746 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
747 """Prepare the dict of values to create the new invoice line for a
748 sales order line. This method may be overridden to implement custom
749 invoice generation (making sure to call super() to establish
750 a clean extension chain).
752 :param browse_record line: sale.order.line record to invoice
753 :param int account_id: optional ID of a G/L account to force
754 (this is used for returning products including service)
755 :return: dict of values to create() the invoice line
758 if not line.invoiced:
761 account_id = line.product_id.property_account_income.id
763 account_id = line.product_id.categ_id.property_account_income_categ.id
765 raise osv.except_osv(_('Error!'),
766 _('Please define income account for this product: "%s" (id:%d).') % \
767 (line.product_id.name, line.product_id.id,))
769 prop = self.pool.get('ir.property').get(cr, uid,
770 'property_account_income_categ', 'product.category',
772 account_id = prop and prop.id or False
773 uosqty = self._get_line_qty(cr, uid, line, context=context)
774 uos_id = self._get_line_uom(cr, uid, line, context=context)
777 pu = round(line.price_unit * line.product_uom_qty / uosqty,
778 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
779 fpos = line.order_id.fiscal_position or False
780 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
782 raise osv.except_osv(_('Error!'),
783 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
786 'sequence': line.sequence,
787 'origin': line.order_id.name,
788 'account_id': account_id,
791 'discount': line.discount,
793 'product_id': line.product_id.id or False,
794 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
795 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
800 def invoice_line_create(self, cr, uid, ids, context=None):
806 for line in self.browse(cr, uid, ids, context=context):
807 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
809 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
810 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
811 sales.add(line.order_id.id)
812 create_ids.append(inv_id)
813 # Trigger workflow events
814 wf_service = netsvc.LocalService("workflow")
815 for sale_id in sales:
816 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
819 def button_cancel(self, cr, uid, ids, context=None):
820 for line in self.browse(cr, uid, ids, context=context):
822 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
823 return self.write(cr, uid, ids, {'state': 'cancel'})
825 def button_confirm(self, cr, uid, ids, context=None):
826 return self.write(cr, uid, ids, {'state': 'confirmed'})
828 def button_done(self, cr, uid, ids, context=None):
829 wf_service = netsvc.LocalService("workflow")
830 res = self.write(cr, uid, ids, {'state': 'done'})
831 for line in self.browse(cr, uid, ids, context=context):
832 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
835 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
836 product_obj = self.pool.get('product.product')
838 return {'value': {'product_uom': product_uos,
839 'product_uom_qty': product_uos_qty}, 'domain': {}}
841 product = product_obj.browse(cr, uid, product_id)
843 'product_uom': product.uom_id.id,
845 # FIXME must depend on uos/uom of the product and not only of the coeff.
848 'product_uom_qty': product_uos_qty / product.uos_coeff,
849 'th_weight': product_uos_qty / product.uos_coeff * product.weight
851 except ZeroDivisionError:
853 return {'value': value}
855 def copy_data(self, cr, uid, id, default=None, context=None):
858 default.update({'state': 'draft', 'invoice_lines': []})
859 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
861 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
862 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
863 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
864 context = context or {}
865 lang = lang or context.get('lang',False)
867 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
869 product_uom_obj = self.pool.get('product.uom')
870 partner_obj = self.pool.get('res.partner')
871 product_obj = self.pool.get('product.product')
872 context = {'lang': lang, 'partner_id': partner_id}
874 lang = partner_obj.browse(cr, uid, partner_id).lang
875 context_partner = {'lang': lang, 'partner_id': partner_id}
878 return {'value': {'th_weight': 0,
879 'product_uos_qty': qty}, 'domain': {'product_uom': [],
882 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
886 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
890 uom2 = product_uom_obj.browse(cr, uid, uom)
891 if product_obj.uom_id.category_id.id != uom2.category_id.id:
894 if product_obj.uos_id:
895 uos2 = product_uom_obj.browse(cr, uid, uos)
896 if product_obj.uos_id.category_id.id != uos2.category_id.id:
900 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
901 if update_tax: #The quantity only have changed
902 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
905 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
906 if product_obj.description_sale:
907 result['name'] += '\n'+product_obj.description_sale
909 if (not uom) and (not uos):
910 result['product_uom'] = product_obj.uom_id.id
911 if product_obj.uos_id:
912 result['product_uos'] = product_obj.uos_id.id
913 result['product_uos_qty'] = qty * product_obj.uos_coeff
914 uos_category_id = product_obj.uos_id.category_id.id
916 result['product_uos'] = False
917 result['product_uos_qty'] = qty
918 uos_category_id = False
919 result['th_weight'] = qty * product_obj.weight
920 domain = {'product_uom':
921 [('category_id', '=', product_obj.uom_id.category_id.id)],
923 [('category_id', '=', uos_category_id)]}
924 elif uos and not uom: # only happens if uom is False
925 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
926 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
927 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
928 elif uom: # whether uos is set or not
929 default_uom = product_obj.uom_id and product_obj.uom_id.id
930 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
931 if product_obj.uos_id:
932 result['product_uos'] = product_obj.uos_id.id
933 result['product_uos_qty'] = qty * product_obj.uos_coeff
935 result['product_uos'] = False
936 result['product_uos_qty'] = qty
937 result['th_weight'] = q * product_obj.weight # Round the quantity up
940 uom2 = product_obj.uom_id
944 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
945 'Please set one before choosing a product.')
946 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
948 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
949 product, qty or 1.0, partner_id, {
950 'uom': uom or result.get('product_uom'),
954 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
955 "You have to change either the product, the quantity or the pricelist.")
957 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
959 result.update({'price_unit': price})
962 'title': _('Configuration Error!'),
963 'message' : warning_msgs
965 return {'value': result, 'domain': domain, 'warning': warning}
967 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
968 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
969 lang=False, update_tax=True, date_order=False, context=None):
970 context = context or {}
971 lang = lang or ('lang' in context and context['lang'])
973 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
974 return self.product_id_change(cursor, user, ids, pricelist, product,
975 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
976 partner_id=partner_id, lang=lang, update_tax=update_tax,
977 date_order=date_order, context=context)
979 def unlink(self, cr, uid, ids, context=None):
982 """Allows to delete sales order lines in draft,cancel states"""
983 for rec in self.browse(cr, uid, ids, context=context):
984 if rec.state not in ['draft', 'cancel']:
985 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
986 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
989 class mail_compose_message(osv.Model):
990 _inherit = 'mail.compose.message'
992 def send_mail(self, cr, uid, ids, context=None):
993 context = context or {}
994 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
995 context = dict(context, mail_post_autofollow=True)
996 wf_service = netsvc.LocalService("workflow")
997 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
998 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1001 class account_invoice(osv.Model):
1002 _inherit = 'account.invoice'
1004 def unlink(self, cr, uid, ids, context=None):
1005 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1006 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1007 #if we can't cancel all invoices, do nothing
1008 if len(invoice_ids) == len(ids):
1009 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1010 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1011 wf_service = netsvc.LocalService("workflow")
1013 wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1014 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1016 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: