1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41 'company_id': fields.many2one('res.company', 'Company', required=False),
44 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
49 class sale_order(osv.osv):
51 _inherit = ['ir.needaction_mixin', 'mail.thread']
52 _description = "Sales Order"
55 def copy(self, cr, uid, id, default=None, context=None):
63 'date_confirm': False,
64 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
66 return super(sale_order, self).copy(cr, uid, id, default, context=context)
68 def _amount_line_tax(self, cr, uid, line, context=None):
70 for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.product_id, line.order_id.partner_id)['taxes']:
71 val += c.get('amount', 0.0)
74 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75 cur_obj = self.pool.get('res.currency')
77 for order in self.browse(cr, uid, ids, context=context):
79 'amount_untaxed': 0.0,
84 cur = order.pricelist_id.currency_id
85 for line in order.order_line:
86 val1 += line.price_subtotal
87 val += self._amount_line_tax(cr, uid, line, context=context)
88 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
94 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
100 tmp[id] = {'picked': 0.0, 'total': 0.0}
102 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
106 stock_picking p on (p.id=m.picking_id)
108 procurement_order mp on (mp.move_id=m.id)
110 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
112 for item in cr.dictfetchall():
113 if item['move_state'] == 'cancel':
116 if item['picking_type'] == 'in':#this is a returned picking
117 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
121 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
125 for order in self.browse(cr, uid, ids, context=context):
127 res[order.id] = 100.0
129 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
132 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
134 for sale in self.browse(cursor, user, ids, context=context):
139 for invoice in sale.invoice_ids:
140 if invoice.state not in ('draft', 'cancel'):
141 tot += invoice.amount_untaxed
143 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
148 def _invoiced(self, cursor, user, ids, name, arg, context=None):
150 for sale in self.browse(cursor, user, ids, context=context):
152 invoice_existence = False
153 for invoice in sale.invoice_ids:
154 if invoice.state!='cancel':
155 invoice_existence = True
156 if invoice.state != 'paid':
159 if not invoice_existence:
163 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
172 clause += 'AND inv.state = \'paid\''
174 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
175 sale_clause = ', sale_order AS sale '
178 cursor.execute('SELECT rel.order_id ' \
179 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180 'WHERE rel.invoice_id = inv.id ' + clause)
181 res = cursor.fetchall()
183 cursor.execute('SELECT sale.id ' \
184 'FROM sale_order AS sale ' \
185 'WHERE sale.id NOT IN ' \
186 '(SELECT rel.order_id ' \
187 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188 res.extend(cursor.fetchall())
190 return [('id', '=', 0)]
191 return [('id', 'in', [x[0] for x in res])]
193 def _get_order(self, cr, uid, ids, context=None):
195 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196 result[line.order_id.id] = True
200 'name': fields.char('Order Reference', size=64, required=True,
201 readonly=True, states={'draft': [('readonly', False)]}, select=True),
202 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
203 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204 'client_order_ref': fields.char('Customer Reference', size=64),
205 'state': fields.selection([
206 ('draft', 'Quotation'),
207 ('waiting_date', 'Waiting Schedule'),
208 ('manual', 'To Invoice'),
209 ('progress', 'In Progress'),
210 ('shipping_except', 'Shipping Exception'),
211 ('invoice_except', 'Invoice Exception'),
213 ('cancel', 'Cancelled')
214 ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
215 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
216 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
217 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
218 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
219 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
220 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
221 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
223 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
224 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225 'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
226 'order_policy': fields.selection([
227 ('prepaid', 'Pay before delivery'),
228 ('manual', 'Deliver & invoice on demand'),
229 ('picking', 'Invoice based on deliveries'),
230 ('postpaid', 'Invoice on order after delivery'),
231 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
232 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
233 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
234 - The 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
235 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
236 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
238 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
240 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
241 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
242 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is a list of picking that has been generated for this sales order."),
243 'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
244 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246 'invoiced': fields.function(_invoiced, string='Paid',
247 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248 'note': fields.text('Notes'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
255 multi='sums', help="The amount without tax."),
256 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
261 multi='sums', help="The tax amount."),
262 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
264 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
267 multi='sums', help="The total amount."),
269 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
270 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
275 'picking_policy': 'direct',
276 'date_order': fields.date.context_today,
277 'order_policy': 'manual',
279 'user_id': lambda obj, cr, uid, context: uid,
280 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281 'invoice_quantity': 'order',
282 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
283 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
286 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
291 def unlink(self, cr, uid, ids, context=None):
292 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
294 for s in sale_orders:
295 if s['state'] in ['draft', 'cancel']:
296 unlink_ids.append(s['id'])
298 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
300 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
302 def onchange_shop_id(self, cr, uid, ids, shop_id):
305 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
306 v['project_id'] = shop.project_id.id
307 # Que faire si le client a une pricelist a lui ?
308 if shop.pricelist_id.id:
309 v['pricelist_id'] = shop.pricelist_id.id
312 def action_cancel_draft(self, cr, uid, ids, context=None):
315 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
316 line_ids = map(lambda x: x[0], cr.fetchall())
317 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
318 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
319 wf_service = netsvc.LocalService("workflow")
321 # Deleting the existing instance of workflow for SO
322 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
323 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
324 self.action_cancel_draft_send_note(cr, uid, ids, context=context)
327 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328 if (not pricelist_id) or (not order_lines):
331 'title': _('Pricelist Warning!'),
332 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
334 return {'warning': warning}
336 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
341 val['partner_invoice_id'] = order_id
343 val['partner_shipping_id'] = order_id
344 return {'value': val}
346 def onchange_partner_id(self, cr, uid, ids, part):
348 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
350 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
351 part = self.pool.get('res.partner').browse(cr, uid, part)
352 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
353 payment_term = part.property_payment_term and part.property_payment_term.id or False
354 fiscal_position = part.property_account_position and part.property_account_position.id or False
355 dedicated_salesman = part.user_id and part.user_id.id or uid
357 'partner_invoice_id': addr['invoice'],
358 'partner_shipping_id': addr['delivery'],
359 'payment_term': payment_term,
360 'fiscal_position': fiscal_position,
361 'user_id': dedicated_salesman,
364 val['pricelist_id'] = pricelist
365 return {'value': val}
367 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
371 if policy == 'prepaid':
373 elif policy == 'picking':
374 inv_qty = 'procurement'
375 return {'value': {'invoice_quantity': inv_qty}}
377 def write(self, cr, uid, ids, vals, context=None):
378 if vals.get('order_policy', False):
379 if vals['order_policy'] == 'prepaid':
380 vals.update({'invoice_quantity': 'order'})
381 elif vals['order_policy'] == 'picking':
382 vals.update({'invoice_quantity': 'procurement'})
383 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
385 def create(self, cr, uid, vals, context=None):
386 if vals.get('order_policy', False):
387 if vals['order_policy'] == 'prepaid':
388 vals.update({'invoice_quantity': 'order'})
389 if vals['order_policy'] == 'picking':
390 vals.update({'invoice_quantity': 'procurement'})
391 order = super(sale_order, self).create(cr, uid, vals, context=context)
393 self.create_send_note(cr, uid, [order], context=context)
396 def button_dummy(self, cr, uid, ids, context=None):
399 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
400 # can be removed after 6.1.
401 def _inv_get(self, cr, uid, order, context=None):
404 def _prepare_invoice(self, cr, uid, order, lines, context=None):
405 """Prepare the dict of values to create the new invoice for a
406 sale order. This method may be overridden to implement custom
407 invoice generation (making sure to call super() to establish
408 a clean extension chain).
410 :param browse_record order: sale.order record to invoice
411 :param list(int) line: list of invoice line IDs that must be
412 attached to the invoice
413 :return: dict of value to create() the invoice
417 journal_ids = self.pool.get('account.journal').search(cr, uid,
418 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
421 raise osv.except_osv(_('Error !'),
422 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
425 'name': order.client_order_ref or '',
426 'origin': order.name,
427 'type': 'out_invoice',
428 'reference': order.client_order_ref or order.name,
429 'account_id': order.partner_id.property_account_receivable.id,
430 'partner_id': order.partner_id.id,
431 'journal_id': journal_ids[0],
432 'invoice_line': [(6, 0, lines)],
433 'currency_id': order.pricelist_id.currency_id.id,
434 'comment': order.note,
435 'payment_term': order.payment_term and order.payment_term.id or False,
436 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
437 'date_invoice': context.get('date_invoice', False),
438 'company_id': order.company_id.id,
439 'user_id': order.user_id and order.user_id.id or False
442 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
443 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
447 def _make_invoice(self, cr, uid, order, lines, context=None):
448 inv_obj = self.pool.get('account.invoice')
449 obj_invoice_line = self.pool.get('account.invoice.line')
452 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
453 from_line_invoice_ids = []
454 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
455 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
456 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
457 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
458 for preinv in order.invoice_ids:
459 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
460 for preline in preinv.invoice_line:
461 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
462 lines.append(inv_line_id)
463 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
464 inv_id = inv_obj.create(cr, uid, inv, context=context)
465 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
466 if data.get('value', False):
467 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
468 inv_obj.button_compute(cr, uid, [inv_id])
471 def manual_invoice(self, cr, uid, ids, context=None):
472 mod_obj = self.pool.get('ir.model.data')
473 wf_service = netsvc.LocalService("workflow")
477 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
478 inv_ids.add(record.id)
479 # inv_ids would have old invoices if any
481 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
482 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
483 inv_ids1.add(record.id)
484 inv_ids = list(inv_ids1.difference(inv_ids))
486 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
487 res_id = res and res[1] or False,
490 'name': _('Customer Invoices'),
494 'res_model': 'account.invoice',
495 'context': "{'type':'out_invoice'}",
496 'type': 'ir.actions.act_window',
499 'res_id': inv_ids and inv_ids[0] or False,
502 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
506 picking_obj = self.pool.get('stock.picking')
507 invoice = self.pool.get('account.invoice')
508 obj_sale_order_line = self.pool.get('sale.order.line')
509 partner_currency = {}
512 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
513 # last day of the last month as invoice date
515 context['date_inv'] = date_inv
516 for o in self.browse(cr, uid, ids, context=context):
517 currency_id = o.pricelist_id.currency_id.id
518 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
519 raise osv.except_osv(
521 _('You cannot group sales having different currencies for the same partner.'))
523 partner_currency[o.partner_id.id] = currency_id
525 for line in o.order_line:
528 elif (line.state in states):
529 lines.append(line.id)
530 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
532 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
534 for o in self.browse(cr, uid, ids, context=context):
535 for i in o.invoice_ids:
536 if i.state == 'draft':
538 for val in invoices.values():
540 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
543 invoice_ref += o.name + '|'
544 self.write(cr, uid, [o.id], {'state': 'progress'})
545 if o.order_policy == 'picking':
546 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
547 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
548 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
550 for order, il in val:
551 res = self._make_invoice(cr, uid, order, il, context=context)
552 invoice_ids.append(res)
553 self.write(cr, uid, [order.id], {'state': 'progress'})
554 if order.order_policy == 'picking':
555 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
556 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
558 self.invoice_send_note(cr, uid, ids, res, context)
561 def action_invoice_cancel(self, cr, uid, ids, context=None):
564 for sale in self.browse(cr, uid, ids, context=context):
565 for line in sale.order_line:
567 # Check if the line is invoiced (has asociated invoice
568 # lines from non-cancelled invoices).
571 for iline in line.invoice_lines:
572 if iline.invoice_id and iline.invoice_id.state != 'cancel':
575 # Update the line (only when needed)
576 if line.invoiced != invoiced:
577 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
578 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
581 def action_invoice_end(self, cr, uid, ids, context=None):
582 for order in self.browse(cr, uid, ids, context=context):
584 # Update the sale order lines state (and invoiced flag).
586 for line in order.order_line:
589 # Check if the line is invoiced (has asociated invoice
590 # lines from non-cancelled invoices).
593 for iline in line.invoice_lines:
594 if iline.invoice_id and iline.invoice_id.state != 'cancel':
597 if line.invoiced != invoiced:
598 vals['invoiced'] = invoiced
599 # If the line was in exception state, now it gets confirmed.
600 if line.state == 'exception':
601 vals['state'] = 'confirmed'
602 # Update the line (only when needed).
604 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
606 # Update the sales order state.
608 if order.state == 'invoice_except':
609 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
612 def action_cancel(self, cr, uid, ids, context=None):
613 wf_service = netsvc.LocalService("workflow")
616 sale_order_line_obj = self.pool.get('sale.order.line')
617 proc_obj = self.pool.get('procurement.order')
618 for sale in self.browse(cr, uid, ids, context=context):
619 for pick in sale.picking_ids:
620 if pick.state not in ('draft', 'cancel'):
621 raise osv.except_osv(
622 _('Could not cancel sales order !'),
623 _('You must first cancel all picking attached to this sales order.'))
624 if pick.state == 'cancel':
625 for mov in pick.move_lines:
626 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
628 for proc in proc_ids:
629 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
630 for r in self.read(cr, uid, ids, ['picking_ids']):
631 for pick in r['picking_ids']:
632 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
633 for inv in sale.invoice_ids:
634 if inv.state not in ('draft', 'cancel'):
635 raise osv.except_osv(
636 _('Could not cancel this sales order !'),
637 _('You must first cancel all invoices attached to this sales order.'))
638 for r in self.read(cr, uid, ids, ['invoice_ids']):
639 for inv in r['invoice_ids']:
640 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
641 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
643 self.cancel_send_note(cr, uid, [sale.id], context=None)
644 self.write(cr, uid, ids, {'state': 'cancel'})
647 def action_wait(self, cr, uid, ids, context=None):
648 for o in self.browse(cr, uid, ids):
650 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
651 if (o.order_policy == 'manual'):
652 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
654 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
655 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
656 self.confirm_send_note(cr, uid, ids, context)
659 def procurement_lines_get(self, cr, uid, ids, *args):
661 for order in self.browse(cr, uid, ids, context={}):
662 for line in order.order_line:
663 if line.procurement_id:
664 res.append(line.procurement_id.id)
667 # if mode == 'finished':
668 # returns True if all lines are done, False otherwise
669 # if mode == 'canceled':
670 # returns True if there is at least one canceled line, False otherwise
671 def test_state(self, cr, uid, ids, mode, *args):
672 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
677 write_cancel_ids = []
678 for order in self.browse(cr, uid, ids, context={}):
679 for line in order.order_line:
680 if (not line.procurement_id) or (line.procurement_id.state=='done'):
681 if line.state != 'done':
682 write_done_ids.append(line.id)
685 if line.procurement_id:
686 if (line.procurement_id.state == 'cancel'):
688 if line.state != 'exception':
689 write_cancel_ids.append(line.id)
693 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
695 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
697 if mode == 'finished':
699 elif mode == 'canceled':
705 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
708 'origin': order.name,
709 'date_planned': date_planned,
710 'product_id': line.product_id.id,
711 'product_qty': line.product_uom_qty,
712 'product_uom': line.product_uom.id,
713 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
714 or line.product_uom_qty,
715 'product_uos': (line.product_uos and line.product_uos.id)\
716 or line.product_uom.id,
717 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
718 'procure_method': line.type,
720 'company_id': order.company_id.id,
724 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
725 location_id = order.shop_id.warehouse_id.lot_stock_id.id
726 output_id = order.shop_id.warehouse_id.lot_output_id.id
728 'name': line.name[:250],
729 'picking_id': picking_id,
730 'product_id': line.product_id.id,
731 'date': date_planned,
732 'date_expected': date_planned,
733 'product_qty': line.product_uom_qty,
734 'product_uom': line.product_uom.id,
735 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
736 'product_uos': (line.product_uos and line.product_uos.id)\
737 or line.product_uom.id,
738 'product_packaging': line.product_packaging.id,
739 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
740 'location_id': location_id,
741 'location_dest_id': output_id,
742 'sale_line_id': line.id,
743 'tracking_id': False,
747 'company_id': order.company_id.id,
748 'price_unit': line.product_id.standard_price or 0.0
751 def _prepare_order_picking(self, cr, uid, order, context=None):
752 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
755 'origin': order.name,
756 'date': order.date_order,
759 'move_type': order.picking_policy,
761 'partner_id': order.partner_shipping_id.id,
763 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
764 'company_id': order.company_id.id,
767 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
768 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
770 Define ship_recreate for process after shipping exception
771 param order: sale order to which the order lines belong
772 param line: sale order line records to procure
773 param move_id: the ID of stock move
774 param proc_id: the ID of procurement
776 move_obj = self.pool.get('stock.move')
777 if order.state == 'shipping_except':
778 for pick in order.picking_ids:
779 for move in pick.move_lines:
780 if move.state == 'cancel':
781 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
783 for mov in move_obj.browse(cr, uid, mov_ids):
784 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
785 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
786 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
789 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
790 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
791 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
794 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
795 """Create the required procurements to supply sale order lines, also connecting
796 the procurements to appropriate stock moves in order to bring the goods to the
797 sale order's requested location.
799 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
800 a standard outgoing picking will be created to wrap the stock moves, as returned
801 by :meth:`~._prepare_order_picking`.
803 Modules that wish to customize the procurements or partition the stock moves over
804 multiple stock pickings may override this method and call ``super()`` with
805 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
807 :param browse_record order: sale order to which the order lines belong
808 :param list(browse_record) order_lines: sale order line records to procure
809 :param int picking_id: optional ID of a stock picking to which the created stock moves
810 will be added. A new picking will be created if ommitted.
813 move_obj = self.pool.get('stock.move')
814 picking_obj = self.pool.get('stock.picking')
815 procurement_obj = self.pool.get('procurement.order')
818 for line in order_lines:
819 if line.state == 'done':
822 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
825 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
827 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
828 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
830 # a service has no stock move
833 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
834 proc_ids.append(proc_id)
835 line.write({'procurement_id': proc_id})
836 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
838 wf_service = netsvc.LocalService("workflow")
840 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
841 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
844 for proc_id in proc_ids:
845 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
848 if order.state == 'shipping_except':
849 val['state'] = 'progress'
850 val['shipped'] = False
852 if (order.order_policy == 'manual'):
853 for line in order.order_line:
854 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
855 val['state'] = 'manual'
860 def action_ship_create(self, cr, uid, ids, context=None):
861 for order in self.browse(cr, uid, ids, context=context):
862 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
865 def action_ship_end(self, cr, uid, ids, context=None):
866 for order in self.browse(cr, uid, ids, context=context):
867 val = {'shipped': True}
868 if order.state == 'shipping_except':
869 val['state'] = 'progress'
870 if (order.order_policy == 'manual'):
871 for line in order.order_line:
872 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
873 val['state'] = 'manual'
875 for line in order.order_line:
877 if line.state == 'exception':
878 towrite.append(line.id)
880 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
881 res = self.write(cr, uid, [order.id], val)
883 self.delivery_end_send_note(cr, uid, [order.id], context=context)
886 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
887 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
889 part = inv['partner_id'] and inv['partner_id'][0]
890 pr = inv['amount_untaxed'] or 0.0
891 partnertype = 'customer'
894 'name': 'Order: '+name,
896 'description': 'Order '+str(inv['id']),
899 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
901 'partner_type': partnertype,
903 'planned_revenue': pr,
907 self.pool.get('res.partner.event').create(cr, uid, event)
909 def has_stockable_products(self, cr, uid, ids, *args):
910 for order in self.browse(cr, uid, ids):
911 for order_line in order.order_line:
912 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
916 # ------------------------------------------------
917 # OpenChatter methods and notifications
918 # ------------------------------------------------
920 def get_needaction_user_ids(self, cr, uid, ids, context=None):
921 result = dict.fromkeys(ids, [])
922 for obj in self.browse(cr, uid, ids, context=context):
923 if (obj.state == 'manual' or obj.state == 'progress'):
924 result[obj.id] = [obj.user_id.id]
927 def create_send_note(self, cr, uid, ids, context=None):
928 for obj in self.browse(cr, uid, ids, context=context):
929 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
930 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
932 def confirm_send_note(self, cr, uid, ids, context=None):
933 for obj in self.browse(cr, uid, ids, context=context):
934 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
936 def cancel_send_note(self, cr, uid, ids, context=None):
937 for obj in self.browse(cr, uid, ids, context=context):
938 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
940 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
941 for order in self.browse(cr, uid, ids, context=context):
942 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
943 # convert datetime field to a datetime, using server format, then
944 # convert it to the user TZ and re-render it with %Z to add the timezone
945 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
946 picking_date_str = fields.datetime.context_timestamp(cr, uid, meeting_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
947 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
949 def delivery_end_send_note(self, cr, uid, ids, context=None):
950 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
952 def invoice_paid_send_note(self, cr, uid, ids, context=None):
953 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
955 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
956 for order in self.browse(cr, uid, ids, context=context):
957 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
958 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
960 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
961 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
966 # TODO add a field price_unit_uos
967 # - update it on change product and unit price
968 # - use it in report if there is a uos
969 class sale_order_line(osv.osv):
971 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
972 tax_obj = self.pool.get('account.tax')
973 cur_obj = self.pool.get('res.currency')
977 for line in self.browse(cr, uid, ids, context=context):
978 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
979 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
980 cur = line.order_id.pricelist_id.currency_id
981 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
984 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
986 for line in self.browse(cr, uid, ids, context=context):
988 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
993 def _get_uom_id(self, cr, uid, *args):
995 proxy = self.pool.get('ir.model.data')
996 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
998 except Exception, ex:
1001 _name = 'sale.order.line'
1002 _description = 'Sales Order Line'
1004 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1005 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1006 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1007 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
1008 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1009 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1010 'invoiced': fields.boolean('Invoiced', readonly=True),
1011 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1012 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1013 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1014 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1015 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1016 help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
1017 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1018 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1019 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1020 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1021 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1022 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1023 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1024 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1025 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1026 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1027 'notes': fields.text('Notes'),
1028 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1029 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
1030 help='* The \'Draft\' state is set when the related sales order in draft state. \
1031 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1032 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1033 \n* The \'Done\' state is set when the sales order line has been picked. \
1034 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1035 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1036 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
1037 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1039 _order = 'sequence, id'
1041 'product_uom' : _get_uom_id,
1044 'product_uom_qty': 1,
1045 'product_uos_qty': 1,
1049 'type': 'make_to_stock',
1050 'product_packaging': False,
1054 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1055 """Prepare the dict of values to create the new invoice line for a
1056 sale order line. This method may be overridden to implement custom
1057 invoice generation (making sure to call super() to establish
1058 a clean extension chain).
1060 :param browse_record line: sale.order.line record to invoice
1061 :param int account_id: optional ID of a G/L account to force
1062 (this is used for returning products including service)
1063 :return: dict of values to create() the invoice line
1066 def _get_line_qty(line):
1067 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1068 if line.product_uos:
1069 return line.product_uos_qty or 0.0
1070 return line.product_uom_qty
1072 return self.pool.get('procurement.order').quantity_get(cr, uid,
1073 line.procurement_id.id, context=context)
1075 def _get_line_uom(line):
1076 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1077 if line.product_uos:
1078 return line.product_uos.id
1079 return line.product_uom.id
1081 return self.pool.get('procurement.order').uom_get(cr, uid,
1082 line.procurement_id.id, context=context)
1084 if not line.invoiced:
1087 account_id = line.product_id.product_tmpl_id.property_account_income.id
1089 account_id = line.product_id.categ_id.property_account_income_categ.id
1091 raise osv.except_osv(_('Error !'),
1092 _('There is no income account defined for this product: "%s" (id:%d)') % \
1093 (line.product_id.name, line.product_id.id,))
1095 prop = self.pool.get('ir.property').get(cr, uid,
1096 'property_account_income_categ', 'product.category',
1098 account_id = prop and prop.id or False
1099 uosqty = _get_line_qty(line)
1100 uos_id = _get_line_uom(line)
1103 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1104 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1105 fpos = line.order_id.fiscal_position or False
1106 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1108 raise osv.except_osv(_('Error !'),
1109 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1112 'origin': line.order_id.name,
1113 'account_id': account_id,
1116 'discount': line.discount,
1118 'product_id': line.product_id.id or False,
1119 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1121 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1126 def invoice_line_create(self, cr, uid, ids, context=None):
1132 for line in self.browse(cr, uid, ids, context=context):
1133 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1135 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1136 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1137 self.write(cr, uid, [line.id], {'invoiced': True})
1138 sales.add(line.order_id.id)
1139 create_ids.append(inv_id)
1140 # Trigger workflow events
1141 wf_service = netsvc.LocalService("workflow")
1142 for sale_id in sales:
1143 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1146 def button_cancel(self, cr, uid, ids, context=None):
1147 for line in self.browse(cr, uid, ids, context=context):
1149 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1150 for move_line in line.move_ids:
1151 if move_line.state != 'cancel':
1152 raise osv.except_osv(
1153 _('Could not cancel sales order line!'),
1154 _('You must first cancel stock moves attached to this sales order line.'))
1155 return self.write(cr, uid, ids, {'state': 'cancel'})
1157 def button_confirm(self, cr, uid, ids, context=None):
1158 return self.write(cr, uid, ids, {'state': 'confirmed'})
1160 def button_done(self, cr, uid, ids, context=None):
1161 wf_service = netsvc.LocalService("workflow")
1162 res = self.write(cr, uid, ids, {'state': 'done'})
1163 for line in self.browse(cr, uid, ids, context=context):
1164 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1167 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1168 product_obj = self.pool.get('product.product')
1170 return {'value': {'product_uom': product_uos,
1171 'product_uom_qty': product_uos_qty}, 'domain': {}}
1173 product = product_obj.browse(cr, uid, product_id)
1175 'product_uom': product.uom_id.id,
1177 # FIXME must depend on uos/uom of the product and not only of the coeff.
1180 'product_uom_qty': product_uos_qty / product.uos_coeff,
1181 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1183 except ZeroDivisionError:
1185 return {'value': value}
1187 def copy_data(self, cr, uid, id, default=None, context=None):
1190 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1191 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1193 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1194 partner_id=False, packaging=False, flag=False, context=None):
1196 return {'value': {'product_packaging': False}}
1197 product_obj = self.pool.get('product.product')
1198 product_uom_obj = self.pool.get('product.uom')
1199 pack_obj = self.pool.get('product.packaging')
1204 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1205 product=product, qty=qty, uom=uom, partner_id=partner_id,
1206 packaging=packaging, flag=False, context=context)
1207 warning_msgs = res.get('warning') and res['warning']['message']
1209 products = product_obj.browse(cr, uid, product, context=context)
1210 if not products.packaging:
1211 packaging = result['product_packaging'] = False
1212 elif not packaging and products.packaging and not flag:
1213 packaging = products.packaging[0].id
1214 result['product_packaging'] = packaging
1217 default_uom = products.uom_id and products.uom_id.id
1218 pack = pack_obj.browse(cr, uid, packaging, context=context)
1219 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1220 # qty = qty - qty % q + q
1221 if qty and (q and not (qty % q) == 0):
1222 ean = pack.ean or _('(n/a)')
1225 if not warning_msgs:
1226 warn_msg = _("You selected a quantity of %d Units.\n"
1227 "But it's not compatible with the selected packaging.\n"
1228 "Here is a proposition of quantities according to the packaging:\n"
1229 "EAN: %s Quantity: %s Type of ul: %s") % \
1230 (qty, ean, qty_pack, type_ul.name)
1231 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1233 'title': _('Configuration Error !'),
1234 'message': warning_msgs
1236 result['product_uom_qty'] = qty
1238 return {'value': result, 'warning': warning}
1240 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1241 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1242 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1243 context = context or {}
1244 lang = lang or context.get('lang',False)
1246 raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1248 product_uom_obj = self.pool.get('product.uom')
1249 partner_obj = self.pool.get('res.partner')
1250 product_obj = self.pool.get('product.product')
1251 context = {'lang': lang, 'partner_id': partner_id}
1253 lang = partner_obj.browse(cr, uid, partner_id).lang
1254 context_partner = {'lang': lang, 'partner_id': partner_id}
1257 return {'value': {'th_weight': 0, 'product_packaging': False,
1258 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1261 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1263 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1264 result = res.get('value', {})
1265 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1266 product_obj = product_obj.browse(cr, uid, product, context=context)
1270 uom2 = product_uom_obj.browse(cr, uid, uom)
1271 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1274 if product_obj.uos_id:
1275 uos2 = product_uom_obj.browse(cr, uid, uos)
1276 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1280 if product_obj.description_sale:
1281 result['notes'] = product_obj.description_sale
1282 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1283 if update_tax: #The quantity only have changed
1284 result['delay'] = (product_obj.sale_delay or 0.0)
1285 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1286 result.update({'type': product_obj.procure_method})
1289 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1291 if (not uom) and (not uos):
1292 result['product_uom'] = product_obj.uom_id.id
1293 if product_obj.uos_id:
1294 result['product_uos'] = product_obj.uos_id.id
1295 result['product_uos_qty'] = qty * product_obj.uos_coeff
1296 uos_category_id = product_obj.uos_id.category_id.id
1298 result['product_uos'] = False
1299 result['product_uos_qty'] = qty
1300 uos_category_id = False
1301 result['th_weight'] = qty * product_obj.weight
1302 domain = {'product_uom':
1303 [('category_id', '=', product_obj.uom_id.category_id.id)],
1305 [('category_id', '=', uos_category_id)]}
1307 elif uos and not uom: # only happens if uom is False
1308 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1309 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1310 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1311 elif uom: # whether uos is set or not
1312 default_uom = product_obj.uom_id and product_obj.uom_id.id
1313 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1314 if product_obj.uos_id:
1315 result['product_uos'] = product_obj.uos_id.id
1316 result['product_uos_qty'] = qty * product_obj.uos_coeff
1318 result['product_uos'] = False
1319 result['product_uos_qty'] = qty
1320 result['th_weight'] = q * product_obj.weight # Round the quantity up
1323 uom2 = product_obj.uom_id
1324 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1325 if (product_obj.type=='product') and int(compare_qty) == -1 \
1326 and (product_obj.procure_method=='make_to_stock'):
1327 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1328 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1329 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1330 max(0,product_obj.qty_available), product_obj.uom_id.name)
1331 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1335 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1336 'Please set one before choosing a product.')
1337 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1339 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1340 product, qty or 1.0, partner_id, {
1341 'uom': uom or result.get('product_uom'),
1345 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1346 "You have to change either the product, the quantity or the pricelist.")
1348 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1350 result.update({'price_unit': price})
1353 'title': _('Configuration Error !'),
1354 'message' : warning_msgs
1356 return {'value': result, 'domain': domain, 'warning': warning}
1358 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1359 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1360 lang=False, update_tax=True, date_order=False, context=None):
1361 context = context or {}
1362 lang = lang or ('lang' in context and context['lang'])
1363 res = self.product_id_change(cursor, user, ids, pricelist, product,
1364 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1365 partner_id=partner_id, lang=lang, update_tax=update_tax,
1366 date_order=date_order, context=context)
1367 if 'product_uom' in res['value']:
1368 del res['value']['product_uom']
1370 res['value']['price_unit'] = 0.0
1373 def unlink(self, cr, uid, ids, context=None):
1376 """Allows to delete sales order lines in draft,cancel states"""
1377 for rec in self.browse(cr, uid, ids, context=context):
1378 if rec.state not in ['draft', 'cancel']:
1379 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1380 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1384 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: