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
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 _description = "Sales Order"
53 def copy(self, cr, uid, id, default=None, context=None):
61 'date_confirm': False,
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context=context)
66 def _amount_line_tax(self, cr, uid, line, context=None):
68 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.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)['taxes']:
69 val += c.get('amount', 0.0)
72 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids, context=context):
77 'amount_untaxed': 0.0,
82 cur = order.pricelist_id.currency_id
83 for line in order.order_line:
84 val1 += line.price_subtotal
85 val += self._amount_line_tax(cr, uid, line, context=context)
86 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
92 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 tmp[id] = {'picked': 0.0, 'total': 0.0}
100 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
104 stock_picking p on (p.id=m.picking_id)
106 procurement_order mp on (mp.move_id=m.id)
108 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
110 for item in cr.dictfetchall():
111 if item['move_state'] == 'cancel':
114 if item['picking_type'] == 'in':#this is a returned picking
115 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
116 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
117 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
119 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
120 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
121 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
123 for order in self.browse(cr, uid, ids, context=context):
125 res[order.id] = 100.0
127 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
137 for invoice in sale.invoice_ids:
138 if invoice.state not in ('draft', 'cancel'):
139 tot += invoice.amount_untaxed
141 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
146 def _invoiced(self, cursor, user, ids, name, arg, context=None):
148 for sale in self.browse(cursor, user, ids, context=context):
150 for invoice in sale.invoice_ids:
151 if invoice.state != 'paid':
154 if not sale.invoice_ids:
158 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
167 clause += 'AND inv.state = \'paid\''
169 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
170 sale_clause = ', sale_order AS sale '
173 cursor.execute('SELECT rel.order_id ' \
174 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
175 'WHERE rel.invoice_id = inv.id ' + clause)
176 res = cursor.fetchall()
178 cursor.execute('SELECT sale.id ' \
179 'FROM sale_order AS sale ' \
180 'WHERE sale.id NOT IN ' \
181 '(SELECT rel.order_id ' \
182 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
183 res.extend(cursor.fetchall())
185 return [('id', '=', 0)]
186 return [('id', 'in', [x[0] for x in res])]
188 def _get_order(self, cr, uid, ids, context=None):
190 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
191 result[line.order_id.id] = True
195 'name': fields.char('Order Reference', size=64, required=True,
196 readonly=True, states={'draft': [('readonly', False)]}, select=True),
197 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
198 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
199 'client_order_ref': fields.char('Customer Reference', size=64),
200 'state': fields.selection([
201 ('draft', 'Quotation'),
202 ('waiting_date', 'Waiting Schedule'),
203 ('manual', 'To Invoice'),
204 ('progress', 'In Progress'),
205 ('shipping_except', 'Shipping Exception'),
206 ('invoice_except', 'Invoice Exception'),
208 ('cancel', 'Cancelled')
209 ], '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),
210 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
211 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
212 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
213 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
214 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
215 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
216 'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact who requested the order or quotation."),
217 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
219 '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."),
220 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
221 '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?"""),
222 'order_policy': fields.selection([
223 ('prepaid', 'Pay before delivery'),
224 ('manual', 'Deliver & invoice on demand'),
225 ('picking', 'Invoice based on deliveries'),
226 ('postpaid', 'Invoice on order after delivery'),
227 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
228 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
229 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
230 - 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.
231 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
232 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
233 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
234 '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."),
236 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
237 '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)."),
238 '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."),
239 '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."),
240 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
241 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
242 'invoiced': fields.function(_invoiced, string='Paid',
243 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
244 'note': fields.text('Notes'),
246 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
248 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
249 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
251 multi='sums', help="The amount without tax."),
252 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
254 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
255 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
257 multi='sums', help="The tax amount."),
258 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
260 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
261 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
263 multi='sums', help="The total amount."),
265 '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)]}),
266 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
267 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
268 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
271 'picking_policy': 'direct',
272 'date_order': fields.date.context_today,
273 'order_policy': 'manual',
275 'user_id': lambda obj, cr, uid, context: uid,
276 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
277 'invoice_quantity': 'order',
278 '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'],
279 'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
280 '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'],
283 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
288 def unlink(self, cr, uid, ids, context=None):
289 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
291 for s in sale_orders:
292 if s['state'] in ['draft', 'cancel']:
293 unlink_ids.append(s['id'])
295 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.'))
297 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
299 def onchange_shop_id(self, cr, uid, ids, shop_id):
302 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
303 v['project_id'] = shop.project_id.id
304 # Que faire si le client a une pricelist a lui ?
305 if shop.pricelist_id.id:
306 v['pricelist_id'] = shop.pricelist_id.id
309 def action_cancel_draft(self, cr, uid, ids, *args):
312 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
313 line_ids = map(lambda x: x[0], cr.fetchall())
314 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
315 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
316 wf_service = netsvc.LocalService("workflow")
318 # Deleting the existing instance of workflow for SO
319 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
320 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
321 for (id,name) in self.name_get(cr, uid, ids):
322 message = _("The sales order '%s' has been set in draft state.") %(name,)
323 self.log(cr, uid, id, message)
326 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327 if (not pricelist_id) or (not order_lines):
330 'title': _('Pricelist Warning!'),
331 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333 return {'warning': warning}
335 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
340 val['partner_invoice_id'] = order_id
342 val['partner_shipping_id'] = order_id
343 return {'value': val}
345 def onchange_partner_id(self, cr, uid, ids, part):
347 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
349 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350 part = self.pool.get('res.partner').browse(cr, uid, part)
351 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352 payment_term = part.property_payment_term and part.property_payment_term.id or False
353 fiscal_position = part.property_account_position and part.property_account_position.id or False
354 dedicated_salesman = part.user_id and part.user_id.id or uid
356 'partner_invoice_id': addr['invoice'],
357 'partner_order_id': addr['contact'],
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 return super(sale_order, self).create(cr, uid, vals, context=context)
393 def button_dummy(self, cr, uid, ids, context=None):
396 #FIXME: the method should return the list of invoices created (invoice_ids)
397 # and not the id of the last invoice created (res). The problem is that we
398 # cannot change it directly since the method is called by the sales order
399 # workflow and I suppose it expects a single id...
400 def _inv_get(self, cr, uid, order, context=None):
403 def _make_invoice(self, cr, uid, order, lines, context=None):
404 journal_obj = self.pool.get('account.journal')
405 inv_obj = self.pool.get('account.invoice')
406 obj_invoice_line = self.pool.get('account.invoice.line')
410 journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
412 raise osv.except_osv(_('Error !'),
413 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
414 a = order.partner_id.property_account_receivable.id
415 pay_term = order.payment_term and order.payment_term.id or False
416 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
417 from_line_invoice_ids = []
418 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
419 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
420 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
421 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
422 for preinv in order.invoice_ids:
423 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
424 for preline in preinv.invoice_line:
425 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
426 lines.append(inv_line_id)
428 'name': order.client_order_ref or '',
429 'origin': order.name,
430 'type': 'out_invoice',
431 'reference': order.client_order_ref or order.name,
433 'partner_id': order.partner_id.id,
434 'journal_id': journal_ids[0],
435 'address_invoice_id': order.partner_invoice_id.id,
436 'address_contact_id': order.partner_order_id.id,
437 'invoice_line': [(6, 0, lines)],
438 'currency_id': order.pricelist_id.currency_id.id,
439 'comment': order.note,
440 'payment_term': pay_term,
441 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
442 'date_invoice': context.get('date_invoice',False),
443 'company_id': order.company_id.id,
444 'user_id': order.user_id and order.user_id.id or False
446 inv.update(self._inv_get(cr, uid, order))
447 inv_id = inv_obj.create(cr, uid, inv, context=context)
448 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime(DEFAULT_SERVER_DATE_FORMAT))
449 if data.get('value', False):
450 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
451 inv_obj.button_compute(cr, uid, [inv_id])
454 def manual_invoice(self, cr, uid, ids, context=None):
455 mod_obj = self.pool.get('ir.model.data')
456 wf_service = netsvc.LocalService("workflow")
460 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
461 inv_ids.add(record.id)
462 # inv_ids would have old invoices if any
464 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
465 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
466 inv_ids1.add(record.id)
467 inv_ids = list(inv_ids1.difference(inv_ids))
469 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
470 res_id = res and res[1] or False,
473 'name': _('Customer Invoices'),
477 'res_model': 'account.invoice',
478 'context': "{'type':'out_invoice'}",
479 'type': 'ir.actions.act_window',
482 'res_id': inv_ids and inv_ids[0] or False,
485 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
489 picking_obj = self.pool.get('stock.picking')
490 invoice = self.pool.get('account.invoice')
491 obj_sale_order_line = self.pool.get('sale.order.line')
492 partner_currency = {}
495 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
496 # last day of the last month as invoice date
498 context['date_inv'] = date_inv
499 for o in self.browse(cr, uid, ids, context=context):
500 currency_id = o.pricelist_id.currency_id.id
501 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
502 raise osv.except_osv(
504 _('You cannot group sales having different currencies for the same partner.'))
506 partner_currency[o.partner_id.id] = currency_id
508 for line in o.order_line:
511 elif (line.state in states):
512 lines.append(line.id)
513 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
515 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
517 for o in self.browse(cr, uid, ids, context=context):
518 for i in o.invoice_ids:
519 if i.state == 'draft':
521 for val in invoices.values():
523 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
526 invoice_ref += o.name + '|'
527 self.write(cr, uid, [o.id], {'state': 'progress'})
528 if o.order_policy == 'picking':
529 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
530 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
531 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
533 for order, il in val:
534 res = self._make_invoice(cr, uid, order, il, context=context)
535 invoice_ids.append(res)
536 self.write(cr, uid, [order.id], {'state': 'progress'})
537 if order.order_policy == 'picking':
538 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
539 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
542 def action_invoice_cancel(self, cr, uid, ids, context=None):
545 for sale in self.browse(cr, uid, ids, context=context):
546 for line in sale.order_line:
548 # Check if the line is invoiced (has asociated invoice
549 # lines from non-cancelled invoices).
552 for iline in line.invoice_lines:
553 if iline.invoice_id and iline.invoice_id.state != 'cancel':
556 # Update the line (only when needed)
557 if line.invoiced != invoiced:
558 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
559 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
562 def action_invoice_end(self, cr, uid, ids, context=None):
563 for order in self.browse(cr, uid, ids, context=context):
565 # Update the sale order lines state (and invoiced flag).
567 for line in order.order_line:
570 # Check if the line is invoiced (has asociated invoice
571 # lines from non-cancelled invoices).
574 for iline in line.invoice_lines:
575 if iline.invoice_id and iline.invoice_id.state != 'cancel':
578 if line.invoiced != invoiced:
579 vals['invoiced'] = invoiced
580 # If the line was in exception state, now it gets confirmed.
581 if line.state == 'exception':
582 vals['state'] = 'confirmed'
583 # Update the line (only when needed).
585 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
587 # Update the sales order state.
589 if order.state == 'invoice_except':
590 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
593 def action_cancel(self, cr, uid, ids, context=None):
594 wf_service = netsvc.LocalService("workflow")
597 sale_order_line_obj = self.pool.get('sale.order.line')
598 proc_obj = self.pool.get('procurement.order')
599 for sale in self.browse(cr, uid, ids, context=context):
600 for pick in sale.picking_ids:
601 if pick.state not in ('draft', 'cancel'):
602 raise osv.except_osv(
603 _('Could not cancel sales order !'),
604 _('You must first cancel all picking attached to this sales order.'))
605 if pick.state == 'cancel':
606 for mov in pick.move_lines:
607 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
609 for proc in proc_ids:
610 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
611 for r in self.read(cr, uid, ids, ['picking_ids']):
612 for pick in r['picking_ids']:
613 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
614 for inv in sale.invoice_ids:
615 if inv.state not in ('draft', 'cancel'):
616 raise osv.except_osv(
617 _('Could not cancel this sales order !'),
618 _('You must first cancel all invoices attached to this sales order.'))
619 for r in self.read(cr, uid, ids, ['invoice_ids']):
620 for inv in r['invoice_ids']:
621 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
622 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
624 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
625 self.log(cr, uid, sale.id, message)
626 self.write(cr, uid, ids, {'state': 'cancel'})
629 def action_wait(self, cr, uid, ids, context=None):
630 for o in self.browse(cr, uid, ids):
632 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
633 if (o.order_policy == 'manual'):
634 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
636 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
637 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
638 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
639 self.log(cr, uid, o.id, message)
642 def procurement_lines_get(self, cr, uid, ids, *args):
644 for order in self.browse(cr, uid, ids, context={}):
645 for line in order.order_line:
646 if line.procurement_id:
647 res.append(line.procurement_id.id)
650 # if mode == 'finished':
651 # returns True if all lines are done, False otherwise
652 # if mode == 'canceled':
653 # returns True if there is at least one canceled line, False otherwise
654 def test_state(self, cr, uid, ids, mode, *args):
655 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
660 write_cancel_ids = []
661 for order in self.browse(cr, uid, ids, context={}):
662 for line in order.order_line:
663 if (not line.procurement_id) or (line.procurement_id.state=='done'):
664 if line.state != 'done':
665 write_done_ids.append(line.id)
668 if line.procurement_id:
669 if (line.procurement_id.state == 'cancel'):
671 if line.state != 'exception':
672 write_cancel_ids.append(line.id)
676 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
678 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
680 if mode == 'finished':
682 elif mode == 'canceled':
688 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
691 'origin': order.name,
692 'date_planned': date_planned,
693 'product_id': line.product_id.id,
694 'product_qty': line.product_uom_qty,
695 'product_uom': line.product_uom.id,
696 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
697 or line.product_uom_qty,
698 'product_uos': (line.product_uos and line.product_uos.id)\
699 or line.product_uom.id,
700 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
701 'procure_method': line.type,
703 'company_id': order.company_id.id,
707 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
708 location_id = order.shop_id.warehouse_id.lot_stock_id.id
709 output_id = order.shop_id.warehouse_id.lot_output_id.id
711 'name': line.name[:250],
712 'picking_id': picking_id,
713 'product_id': line.product_id.id,
714 'date': date_planned,
715 'date_expected': date_planned,
716 'product_qty': line.product_uom_qty,
717 'product_uom': line.product_uom.id,
718 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
719 'product_uos': (line.product_uos and line.product_uos.id)\
720 or line.product_uom.id,
721 'product_packaging': line.product_packaging.id,
722 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
723 'location_id': location_id,
724 'location_dest_id': output_id,
725 'sale_line_id': line.id,
726 'tracking_id': False,
730 'company_id': order.company_id.id,
731 'price_unit': line.product_id.standard_price or 0.0
734 def _prepare_order_picking(self, cr, uid, order, context=None):
735 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
738 'origin': order.name,
739 'date': order.date_order,
742 'move_type': order.picking_policy,
744 'address_id': order.partner_shipping_id.id,
746 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
747 'company_id': order.company_id.id,
750 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
751 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
753 Define ship_recreate for process after shipping exception
754 param order: sale order to which the order lines belong
755 param line: sale order line records to procure
756 param move_id: the ID of stock move
757 param proc_id: the ID of procurement
759 move_obj = self.pool.get('stock.move')
760 if order.state == 'shipping_except':
761 for pick in order.picking_ids:
762 for move in pick.move_lines:
763 if move.state == 'cancel':
764 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
766 for mov in move_obj.browse(cr, uid, mov_ids):
767 # 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?
768 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
769 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
772 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
773 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
774 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
777 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
778 """Create the required procurements to supply sale order lines, also connecting
779 the procurements to appropriate stock moves in order to bring the goods to the
780 sale order's requested location.
782 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
783 a standard outgoing picking will be created to wrap the stock moves, as returned
784 by :meth:`~._prepare_order_picking`.
786 Modules that wish to customize the procurements or partition the stock moves over
787 multiple stock pickings may override this method and call ``super()`` with
788 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
790 :param browse_record order: sale order to which the order lines belong
791 :param list(browse_record) order_lines: sale order line records to procure
792 :param int picking_id: optional ID of a stock picking to which the created stock moves
793 will be added. A new picking will be created if ommitted.
796 move_obj = self.pool.get('stock.move')
797 picking_obj = self.pool.get('stock.picking')
798 procurement_obj = self.pool.get('procurement.order')
801 for line in order_lines:
802 if line.state == 'done':
805 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
808 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
810 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
811 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
813 # a service has no stock move
816 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
817 proc_ids.append(proc_id)
818 line.write({'procurement_id': proc_id})
819 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
821 wf_service = netsvc.LocalService("workflow")
823 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
825 for proc_id in proc_ids:
826 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
829 if order.state == 'shipping_except':
830 val['state'] = 'progress'
831 val['shipped'] = False
833 if (order.order_policy == 'manual'):
834 for line in order.order_line:
835 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
836 val['state'] = 'manual'
841 def action_ship_create(self, cr, uid, ids, context=None):
842 for order in self.browse(cr, uid, ids, context=context):
843 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
846 def action_ship_end(self, cr, uid, ids, context=None):
847 for order in self.browse(cr, uid, ids, context=context):
848 val = {'shipped': True}
849 if order.state == 'shipping_except':
850 val['state'] = 'progress'
851 if (order.order_policy == 'manual'):
852 for line in order.order_line:
853 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
854 val['state'] = 'manual'
856 for line in order.order_line:
858 if line.state == 'exception':
859 towrite.append(line.id)
861 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
862 self.write(cr, uid, [order.id], val)
865 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
866 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
868 part = inv['partner_id'] and inv['partner_id'][0]
869 pr = inv['amount_untaxed'] or 0.0
870 partnertype = 'customer'
873 'name': 'Order: '+name,
875 'description': 'Order '+str(inv['id']),
878 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
880 'partner_type': partnertype,
882 'planned_revenue': pr,
886 self.pool.get('res.partner.event').create(cr, uid, event)
888 def has_stockable_products(self, cr, uid, ids, *args):
889 for order in self.browse(cr, uid, ids):
890 for order_line in order.order_line:
891 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
896 # TODO add a field price_unit_uos
897 # - update it on change product and unit price
898 # - use it in report if there is a uos
899 class sale_order_line(osv.osv):
901 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
902 tax_obj = self.pool.get('account.tax')
903 cur_obj = self.pool.get('res.currency')
907 for line in self.browse(cr, uid, ids, context=context):
908 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
909 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
910 cur = line.order_id.pricelist_id.currency_id
911 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
914 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
916 for line in self.browse(cr, uid, ids, context=context):
918 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
923 def _get_uom_id(self, cr, uid, *args):
925 proxy = self.pool.get('ir.model.data')
926 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
928 except Exception, ex:
931 _name = 'sale.order.line'
932 _description = 'Sales Order Line'
934 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
935 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
936 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
937 '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)]}),
938 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
939 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
940 'invoiced': fields.boolean('Invoiced', readonly=True),
941 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
942 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
943 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
944 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
945 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
946 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."),
947 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
948 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
949 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
950 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
951 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
952 'product_uos': fields.many2one('product.uom', 'Product UoS'),
953 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
954 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
955 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
956 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
957 'notes': fields.text('Notes'),
958 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
959 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
960 help='* The \'Draft\' state is set when the related sales order in draft state. \
961 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
962 \n* The \'Exception\' state is set when the related sales order is set as exception. \
963 \n* The \'Done\' state is set when the sales order line has been picked. \
964 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
965 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
966 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
967 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
969 _order = 'sequence, id'
971 'product_uom' : _get_uom_id,
974 'product_uom_qty': 1,
975 'product_uos_qty': 1,
979 'type': 'make_to_stock',
980 'product_packaging': False,
984 def _prepare_order_line_invoice_line(self, cr, uid, ids, line, account_id=False, context=None):
985 """ Builds the invoice line dict from a sale order line
986 @param line: sale order line object
987 @param account_id: the id of the account to force eventually (the method is used for picking return including service)
988 @return: dict that will be used to create the invoice line
991 def _get_line_qty(line):
992 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
994 return line.product_uos_qty or 0.0
995 return line.product_uom_qty
997 return self.pool.get('procurement.order').quantity_get(cr, uid,
998 line.procurement_id.id, context=context)
1000 def _get_line_uom(line):
1001 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1002 if line.product_uos:
1003 return line.product_uos.id
1004 return line.product_uom.id
1006 return self.pool.get('procurement.order').uom_get(cr, uid,
1007 line.procurement_id.id, context=context)
1009 if not line.invoiced:
1012 account_id = line.product_id.product_tmpl_id.property_account_income.id
1014 account_id = line.product_id.categ_id.property_account_income_categ.id
1016 raise osv.except_osv(_('Error !'),
1017 _('There is no income account defined for this product: "%s" (id:%d)') % \
1018 (line.product_id.name, line.product_id.id,))
1020 prop = self.pool.get('ir.property').get(cr, uid,
1021 'property_account_income_categ', 'product.category',
1023 account_id = prop and prop.id or False
1024 uosqty = _get_line_qty(line)
1025 uos_id = _get_line_uom(line)
1028 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1029 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1030 fpos = line.order_id.fiscal_position or False
1031 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1033 raise osv.except_osv(_('Error !'),
1034 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1037 'origin': line.order_id.name,
1038 'account_id': account_id,
1041 'discount': line.discount,
1043 'product_id': line.product_id.id or False,
1044 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1046 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1051 def invoice_line_create(self, cr, uid, ids, context=None):
1057 for line in self.browse(cr, uid, ids, context=context):
1058 vals = self._prepare_order_line_invoice_line(cr, uid, ids, line, False, context)
1060 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1061 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1062 self.write(cr, uid, [line.id], {'invoiced': True})
1063 sales.add(line.order_id.id)
1064 create_ids.append(inv_id)
1065 # Trigger workflow events
1066 wf_service = netsvc.LocalService("workflow")
1067 for sale_id in sales:
1068 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1071 def button_cancel(self, cr, uid, ids, context=None):
1072 for line in self.browse(cr, uid, ids, context=context):
1074 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1075 for move_line in line.move_ids:
1076 if move_line.state != 'cancel':
1077 raise osv.except_osv(
1078 _('Could not cancel sales order line!'),
1079 _('You must first cancel stock moves attached to this sales order line.'))
1080 return self.write(cr, uid, ids, {'state': 'cancel'})
1082 def button_confirm(self, cr, uid, ids, context=None):
1083 return self.write(cr, uid, ids, {'state': 'confirmed'})
1085 def button_done(self, cr, uid, ids, context=None):
1086 wf_service = netsvc.LocalService("workflow")
1087 res = self.write(cr, uid, ids, {'state': 'done'})
1088 for line in self.browse(cr, uid, ids, context=context):
1089 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1092 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1093 product_obj = self.pool.get('product.product')
1095 return {'value': {'product_uom': product_uos,
1096 'product_uom_qty': product_uos_qty}, 'domain': {}}
1098 product = product_obj.browse(cr, uid, product_id)
1100 'product_uom': product.uom_id.id,
1102 # FIXME must depend on uos/uom of the product and not only of the coeff.
1105 'product_uom_qty': product_uos_qty / product.uos_coeff,
1106 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1108 except ZeroDivisionError:
1110 return {'value': value}
1112 def copy_data(self, cr, uid, id, default=None, context=None):
1115 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1116 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1118 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1119 partner_id=False, packaging=False, flag=False, context=None):
1121 return {'value': {'product_packaging': False}}
1122 product_obj = self.pool.get('product.product')
1123 product_uom_obj = self.pool.get('product.uom')
1124 pack_obj = self.pool.get('product.packaging')
1129 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1130 product=product, qty=qty, uom=uom, partner_id=partner_id,
1131 packaging=packaging, flag=False, context=context)
1132 warning_msgs = res.get('warning') and res['warning']['message']
1134 products = product_obj.browse(cr, uid, product, context=context)
1135 if not products.packaging:
1136 packaging = result['product_packaging'] = False
1137 elif not packaging and products.packaging and not flag:
1138 packaging = products.packaging[0].id
1139 result['product_packaging'] = packaging
1142 default_uom = products.uom_id and products.uom_id.id
1143 pack = pack_obj.browse(cr, uid, packaging, context=context)
1144 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1145 # qty = qty - qty % q + q
1146 if qty and (q and not (qty % q) == 0):
1147 ean = pack.ean or _('(n/a)')
1150 if not warning_msgs:
1151 warn_msg = _("You selected a quantity of %d Units.\n"
1152 "But it's not compatible with the selected packaging.\n"
1153 "Here is a proposition of quantities according to the packaging:\n"
1154 "EAN: %s Quantity: %s Type of ul: %s") % \
1155 (qty, ean, qty_pack, type_ul.name)
1156 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1158 'title': _('Configuration Error !'),
1159 'message': warning_msgs
1161 result['product_uom_qty'] = qty
1163 return {'value': result, 'warning': warning}
1165 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1166 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1167 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1168 context = context or {}
1169 lang = lang or context.get('lang',False)
1171 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.'))
1173 product_uom_obj = self.pool.get('product.uom')
1174 partner_obj = self.pool.get('res.partner')
1175 product_obj = self.pool.get('product.product')
1176 context = {'lang': lang, 'partner_id': partner_id}
1178 lang = partner_obj.browse(cr, uid, partner_id).lang
1179 context_partner = {'lang': lang, 'partner_id': partner_id}
1182 return {'value': {'th_weight': 0, 'product_packaging': False,
1183 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1186 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1188 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1189 result = res.get('value', {})
1190 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1191 product_obj = product_obj.browse(cr, uid, product, context=context)
1195 uom2 = product_uom_obj.browse(cr, uid, uom)
1196 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1199 if product_obj.uos_id:
1200 uos2 = product_uom_obj.browse(cr, uid, uos)
1201 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1205 if product_obj.description_sale:
1206 result['notes'] = product_obj.description_sale
1207 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1208 if update_tax: #The quantity only have changed
1209 result['delay'] = (product_obj.sale_delay or 0.0)
1210 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1211 result.update({'type': product_obj.procure_method})
1214 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1216 if (not uom) and (not uos):
1217 result['product_uom'] = product_obj.uom_id.id
1218 if product_obj.uos_id:
1219 result['product_uos'] = product_obj.uos_id.id
1220 result['product_uos_qty'] = qty * product_obj.uos_coeff
1221 uos_category_id = product_obj.uos_id.category_id.id
1223 result['product_uos'] = False
1224 result['product_uos_qty'] = qty
1225 uos_category_id = False
1226 result['th_weight'] = qty * product_obj.weight
1227 domain = {'product_uom':
1228 [('category_id', '=', product_obj.uom_id.category_id.id)],
1230 [('category_id', '=', uos_category_id)]}
1232 elif uos and not uom: # only happens if uom is False
1233 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1234 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1235 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1236 elif uom: # whether uos is set or not
1237 default_uom = product_obj.uom_id and product_obj.uom_id.id
1238 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1239 if product_obj.uos_id:
1240 result['product_uos'] = product_obj.uos_id.id
1241 result['product_uos_qty'] = qty * product_obj.uos_coeff
1243 result['product_uos'] = False
1244 result['product_uos_qty'] = qty
1245 result['th_weight'] = q * product_obj.weight # Round the quantity up
1248 uom2 = product_obj.uom_id
1249 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1250 and (product_obj.procure_method=='make_to_stock'):
1251 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1252 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1253 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1254 max(0,product_obj.qty_available), product_obj.uom_id.name)
1255 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1259 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1260 'Please set one before choosing a product.')
1261 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1263 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1264 product, qty or 1.0, partner_id, {
1265 'uom': uom or result.get('product_uom'),
1269 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1270 "You have to change either the product, the quantity or the pricelist.")
1272 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1274 result.update({'price_unit': price})
1277 'title': _('Configuration Error !'),
1278 'message' : warning_msgs
1280 return {'value': result, 'domain': domain, 'warning': warning}
1282 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1283 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1284 lang=False, update_tax=True, date_order=False, context=None):
1285 context = context or {}
1286 lang = lang or ('lang' in context and context['lang'])
1287 res = self.product_id_change(cursor, user, ids, pricelist, product,
1288 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1289 partner_id=partner_id, lang=lang, update_tax=update_tax,
1290 date_order=date_order, context=context)
1291 if 'product_uom' in res['value']:
1292 del res['value']['product_uom']
1294 res['value']['price_unit'] = 0.0
1297 def unlink(self, cr, uid, ids, context=None):
1300 """Allows to delete sales order lines in draft,cancel states"""
1301 for rec in self.browse(cr, uid, ids, context=context):
1302 if rec.state not in ['draft', 'cancel']:
1303 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1304 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1308 class sale_config_picking_policy(osv.osv_memory):
1309 _name = 'sale.config.picking_policy'
1310 _inherit = 'res.config'
1313 'name': fields.char('Name', size=64),
1314 'sale_orders': fields.boolean('Based on Sales Orders',),
1315 'deli_orders': fields.boolean('Based on Delivery Orders'),
1316 'task_work': fields.boolean('Based on Tasks\' Work'),
1317 'timesheet': fields.boolean('Based on Timesheet'),
1318 'order_policy': fields.selection([
1319 ('manual', 'Invoice Based on Sales Orders'),
1320 ('picking', 'Invoice Based on Deliveries'),
1321 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1322 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1323 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1326 'order_policy': 'manual',
1327 'time_unit': lambda self, cr, uid, c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c) and self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c)[0] or False,
1330 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1333 res.update({'order_policy': 'manual'})
1335 res.update({'order_policy': 'picking'})
1336 return {'value':res}
1338 def execute(self, cr, uid, ids, context=None):
1339 ir_values_obj = self.pool.get('ir.values')
1340 data_obj = self.pool.get('ir.model.data')
1341 menu_obj = self.pool.get('ir.ui.menu')
1342 module_obj = self.pool.get('ir.module.module')
1343 module_upgrade_obj = self.pool.get('base.module.upgrade')
1346 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1348 wizard = self.browse(cr, uid, ids)[0]
1350 if wizard.sale_orders:
1351 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1352 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1354 if wizard.deli_orders:
1355 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1356 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1358 if wizard.task_work:
1359 module_name.append('project_timesheet')
1360 module_name.append('project_mrp')
1361 module_name.append('account_analytic_analysis')
1363 if wizard.timesheet:
1364 module_name.append('account_analytic_analysis')
1366 if wizard.charge_delivery:
1367 module_name.append('delivery')
1369 if len(module_name):
1371 need_install = False
1373 for module in module_name:
1374 data_id = module_obj.name_search(cr, uid , module, [], '=')
1375 module_ids.append(data_id[0][0])
1377 for module in module_obj.browse(cr, uid, module_ids):
1378 if module.state == 'uninstalled':
1379 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1383 pooler.restart_pool(cr.dbname, update_module=True)[1]
1385 if wizard.time_unit:
1386 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1387 product_obj = self.pool.get('product.product')
1388 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1390 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1391 if wizard.task_work and wizard.time_unit:
1392 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1393 self.pool.get('res.company').write(cr, uid, [company_id], {
1394 'project_time_mode_id': wizard.time_unit.id
1397 sale_config_picking_policy()
1399 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: