1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ##############################################################################
31 from mx import DateTime
34 from osv import fields,osv
36 from tools import config
37 from tools.translate import _
39 from xml.dom import minidom
41 #----------------------------------------------------------
43 #----------------------------------------------------------
44 class stock_incoterms(osv.osv):
45 _name = "stock.incoterms"
46 _description = "Incoterms"
48 'name': fields.char('Name', size=64, required=True),
49 'code': fields.char('Code', size=3, required=True),
50 'active': fields.boolean('Active'),
53 'active': lambda *a: True,
57 #----------------------------------------------------------
59 #----------------------------------------------------------
60 class stock_location(osv.osv):
61 _name = "stock.location"
62 _description = "Location"
63 _parent_name = "location_id"
65 _parent_order = 'name'
66 _order = 'parent_left'
68 def _complete_name(self, cr, uid, ids, name, args, context):
69 def _get_one_full_name(location, level=4):
70 if location.location_id:
71 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
74 return parent_path + location.name
76 for m in self.browse(cr, uid, ids, context=context):
77 res[m.id] = _get_one_full_name(m)
80 def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
83 res[id] = {}.fromkeys(field_names, 0.0)
84 if ('product_id' not in context) or not ids:
86 #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
88 context['location'] = [loc]
89 prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
90 if 'stock_real' in field_names:
91 res[loc]['stock_real'] = prod.qty_available
92 if 'stock_virtual' in field_names:
93 res[loc]['stock_virtual'] = prod.virtual_available
97 'name': fields.char('Location Name', size=64, required=True, translate=True),
98 'active': fields.boolean('Active'),
99 'usage': fields.selection([('supplier','Supplier Location'),('view','View'),('internal','Internal Location'),('customer','Customer Location'),('inventory','Inventory'),('procurement','Procurement'),('production','Production')], 'Location type', required=True),
100 'allocation_method': fields.selection([('fifo','FIFO'),('lifo','LIFO'),('nearest','Nearest')], 'Allocation Method', required=True),
102 'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
104 'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
105 'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
107 'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type','!=','view')]),
108 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
109 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
111 'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
112 'chained_location_type': fields.selection([('','None'),('customer', 'Customer'),('fixed','Fixed Location')],
113 'Chained Location Type', required=True),
114 'chained_auto_packing': fields.selection(
115 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
118 help="This is used only if you selected a chained location type.\n" \
119 "The 'Automatic Move' value will create a stock move after the current one that will be "\
120 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
121 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
123 'chained_delay': fields.integer('Chained Delay (days)'),
124 'address_id': fields.many2one('res.partner.address', 'Location Address'),
125 'icon': fields.selection(tools.icons, 'Icon', size=64),
127 'comment': fields.text('Additional Information'),
128 'posx': fields.integer('Corridor (X)'),
129 'posy': fields.integer('Shelves (Y)'),
130 'posz': fields.integer('Height (Z)'),
132 'parent_left': fields.integer('Left Parent', select=1),
133 'parent_right': fields.integer('Right Parent', select=1),
136 'active': lambda *a: 1,
137 'usage': lambda *a: 'internal',
138 'allocation_method': lambda *a: 'fifo',
139 'chained_location_type': lambda *a: '',
140 'chained_auto_packing': lambda *a: 'manual',
141 'posx': lambda *a: 0,
142 'posy': lambda *a: 0,
143 'posz': lambda *a: 0,
144 'icon': lambda *a: False
147 def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
149 if location.chained_location_type=='customer':
151 result = partner.property_stock_customer
152 elif location.chained_location_type=='fixed':
153 result = location.chained_location_id
155 return result, location.chained_auto_packing, location.chained_delay
158 def picking_type_get(self, cr, uid, from_location, to_location, context={}):
160 if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer','supplier')):
162 elif (from_location.usage in ('supplier','customer')) and (to_location.usage=='internal'):
166 def _product_get_all_report(self, cr, uid, ids, product_ids=False,
168 return self._product_get_report(cr, uid, ids, product_ids, context,
171 def _product_get_report(self, cr, uid, ids, product_ids=False,
172 context=None, recursive=False):
175 product_obj = self.pool.get('product.product')
177 product_ids = product_obj.search(cr, uid, [])
179 products = product_obj.browse(cr, uid, product_ids, context=context)
182 for product in products:
183 products_by_uom.setdefault(product.uom_id.id, [])
184 products_by_uom[product.uom_id.id].append(product)
185 products_by_id.setdefault(product.id, [])
186 products_by_id[product.id] = product
190 for uom_id in products_by_uom.keys():
191 fnc = self._product_get
193 fnc = self._product_all_get
196 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
198 for product_id in qty.keys():
199 if not qty[product_id]:
201 product = products_by_id[product_id]
203 'price': product.standard_price,
204 'name': product.name,
205 'code': product.default_code, # used by lot_overview_all report!
206 'variants': product.variants or '',
207 'uom': product.uom_id.name,
208 'amount': qty[product_id],
212 def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):
213 product_obj = self.pool.get('product.product')
219 return product_obj.get_product_available(cr,uid,product_ids,context=context)
221 def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
222 ids = id and [id] or []
223 return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
225 def _product_all_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
226 # build the list of ids of children of the location given by id
227 ids = id and [id] or []
228 location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
229 return self._product_get_multi_location(cr, uid, location_ids, product_ids, context, states)
231 def _product_virtual_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
232 return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed','waiting','assigned','done'])
236 # Improve this function
239 # [ (tracking_id, product_qty, location_id) ]
241 def _product_reserve(self, cr, uid, ids, product_id, product_qty, context={}):
244 for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
245 cr.execute("select product_uom,sum(product_qty) as product_qty from stock_move where location_dest_id=%d and product_id=%d and state='done' group by product_uom", (id,product_id))
246 results = cr.dictfetchall()
247 cr.execute("select product_uom,-sum(product_qty) as product_qty from stock_move where location_id=%d and product_id=%d and state in ('done', 'assigned') group by product_uom", (id,product_id))
248 results += cr.dictfetchall()
253 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'],r['product_qty'], context.get('uom',False))
262 if amount>min(total,product_qty):
263 amount = min(product_qty,total)
264 result.append((amount,id))
265 product_qty -= amount
274 class stock_tracking(osv.osv):
275 _name = "stock.tracking"
276 _description = "Stock Tracking Lots"
279 salt = '31' * 8 + '3'
281 for sscc_part, salt_part in zip(sscc, salt):
282 sum += int(sscc_part) * int(salt_part)
283 return (10 - (sum % 10)) % 10
284 checksum = staticmethod(checksum)
286 def make_sscc(self, cr, uid, context={}):
287 sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
288 return sequence + str(self.checksum(sequence))
291 'name': fields.char('Tracking', size=64, required=True),
292 'active': fields.boolean('Active'),
293 'serial': fields.char('Reference', size=64),
294 'move_ids' : fields.one2many('stock.move', 'tracking_id', 'Moves tracked'),
295 'date': fields.datetime('Date create', required=True),
298 'active': lambda *a: 1,
300 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
303 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
308 ids = self.search(cr, user, [('serial','=',name)]+ args, limit=limit, context=context)
309 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
310 return self.name_get(cr, user, ids, context)
312 def name_get(self, cr, uid, ids, context={}):
315 res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name','serial'], context)]
318 def unlink(self, cr ,uid, ids):
319 raise Exception, _('You can not remove a lot line !')
322 #----------------------------------------------------------
324 #----------------------------------------------------------
325 class stock_picking(osv.osv):
326 _name = "stock.picking"
327 _description = "Packing list"
328 def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
329 if not value: return False
330 if isinstance(ids, (int, long)):
332 for pick in self.browse(cr, uid, ids, context):
333 sql_str="""update stock_move set
336 picking_id=%d """ % (value,pick.id)
339 sql_str += " and (date_planned='"+pick.max_date+"' or date_planned>'"+value+"')"
343 def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
344 if not value: return False
345 if isinstance(ids, (int, long)):
347 for pick in self.browse(cr, uid, ids, context):
348 sql_str="""update stock_move set
351 picking_id=%d """ % (value,pick.id)
353 sql_str += " and (date_planned='"+pick.min_date+"' or date_planned<'"+value+"')"
357 def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
360 res[id] = {'min_date':False, 'max_date': False}
370 picking_id in (""" + ','.join(map(str, ids)) + """)
373 for pick, dt1,dt2 in cr.fetchall():
374 res[pick]['min_date'] = dt1
375 res[pick]['max_date'] = dt2
379 'name': fields.char('Reference', size=64, required=True, select=True),
380 'origin': fields.char('Origin Reference', size=64),
381 'backorder_id': fields.many2one('stock.picking', 'Back Order'),
382 'type': fields.selection([('out','Sending Goods'),('in','Getting Goods'),('internal','Internal'),('delivery','Delivery')], 'Shipping Type', required=True, select=True),
383 'active': fields.boolean('Active'),
384 'note': fields.text('Notes'),
386 'location_id': fields.many2one('stock.location', 'Location'),
387 'location_dest_id': fields.many2one('stock.location', 'Dest. Location'),
388 'move_type': fields.selection([('direct','Direct Delivery'),('one','All at once')],'Delivery Method', required=True),
389 'state': fields.selection([
392 ('confirmed','Confirmed'),
393 ('assigned','Assigned'),
396 ], 'Status', readonly=True, select=True),
397 'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
398 method=True,store=True, type='datetime', string='Planned Date', select=1),
399 'date':fields.datetime('Date Order'),
400 'date_done':fields.datetime('Date Done'),
401 'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
402 method=True,store=True, type='datetime', string='Max. Planned Date', select=2),
403 'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines'),
405 'auto_picking': fields.boolean('Auto-Packing'),
406 'address_id': fields.many2one('res.partner.address', 'Partner'),
407 'invoice_state':fields.selection([
408 ("invoiced","Invoiced"),
409 ("2binvoiced","To be invoiced"),
410 ("none","Not from Packing")], "Invoice Status",
411 select=True, required=True, readonly=True, states={'draft':[('readonly',False)]}),
414 'name': lambda self,cr,uid,context: self.pool.get('ir.sequence').get(cr, uid, 'stock.picking'),
415 'active': lambda *a: 1,
416 'state': lambda *a: 'draft',
417 'move_type': lambda *a: 'direct',
418 'type': lambda *a: 'in',
419 'invoice_state': lambda *a: 'none',
420 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
422 #def copy(self, cr, uid, id, data=None, context={}):
424 # return super(stock_picking, self).copy(cr, uid, id, data, context)
426 def onchange_partner_in(self, cr, uid, context, partner_id=None):
427 sid = self.pool.get('res.partner.address').browse(cr, uid, partner_id, context).partner_id.property_stock_supplier.id
430 def action_explode(self, cr, uid, moves, context={}):
433 def action_confirm(self, cr, uid, ids, context={}):
434 self.write(cr, uid, ids, {'state': 'confirmed'})
436 for picking in self.browse(cr, uid, ids):
437 for r in picking.move_lines:
440 todo = self.action_explode(cr, uid, todo, context)
442 self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
445 def test_auto_picking(self, cr, uid, ids):
446 # TODO: Check locations to see if in the same location ?
449 def button_confirm(self, cr, uid, ids, *args):
451 wf_service = netsvc.LocalService("workflow")
452 wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
453 self.force_assign(cr, uid, ids, *args)
456 def action_assign(self, cr, uid, ids, *args):
457 for pick in self.browse(cr, uid, ids):
458 move_ids = [x.id for x in pick.move_lines if x.state=='confirmed']
459 self.pool.get('stock.move').action_assign(cr, uid, move_ids)
462 def force_assign(self, cr, uid, ids, *args):
463 wf_service = netsvc.LocalService("workflow")
464 for pick in self.browse(cr, uid, ids):
465 # move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
466 move_ids = [x.id for x in pick.move_lines]
467 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
468 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
471 def draft_force_assign(self, cr, uid, ids, *args):
472 wf_service = netsvc.LocalService("workflow")
473 for pick in self.browse(cr, uid, ids):
474 wf_service.trg_validate(uid, 'stock.picking', pick.id,
475 'button_confirm', cr)
476 move_ids = [x.id for x in pick.move_lines]
477 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
478 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
481 def draft_validate(self, cr, uid, ids, *args):
482 wf_service = netsvc.LocalService("workflow")
483 self.draft_force_assign(cr, uid, ids)
484 for pick in self.browse(cr, uid, ids):
485 self.action_move(cr, uid, [pick.id])
486 wf_service.trg_validate(uid, 'stock.picking', pick.id , 'button_done', cr)
489 def cancel_assign(self, cr, uid, ids, *args):
490 wf_service = netsvc.LocalService("workflow")
491 for pick in self.browse(cr, uid, ids):
492 move_ids = [x.id for x in pick.move_lines]
493 self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
494 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
497 def action_assign_wkf(self, cr, uid, ids):
498 self.write(cr, uid, ids, {'state':'assigned'})
501 def test_finnished(self, cr, uid, ids):
502 move_ids=self.pool.get('stock.move').search(cr,uid,[('picking_id','in',ids)])
504 for move in self.pool.get('stock.move').browse(cr,uid,move_ids):
505 if move.state not in ('done','cancel') :
506 if move.product_qty != 0.0:
509 move.write(cr,uid,[move.id],{'state':'done'})
512 def test_assigned(self, cr, uid, ids):
514 for pick in self.browse(cr, uid, ids):
516 for move in pick.move_lines:
517 if (move.state in ('confirmed','draft')) and (mt=='one'):
519 if (mt=='direct') and (move.state=='assigned') and (move.product_qty):
521 ok = ok and (move.state in ('cancel','done','assigned'))
524 def action_cancel(self, cr, uid, ids, context={}):
525 for pick in self.browse(cr, uid, ids):
526 ids2 = [move.id for move in pick.move_lines]
527 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
528 self.write(cr,uid, ids, {'state':'cancel', 'invoice_state':'none'})
532 # TODO: change and create a move if not parents
534 def action_done(self, cr, uid, ids, context=None):
535 self.write(cr,uid, ids, {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
538 def action_move(self, cr, uid, ids, context={}):
539 for pick in self.browse(cr, uid, ids):
541 for move in pick.move_lines:
542 if move.state=='assigned':
546 self.pool.get('stock.move').action_done(cr, uid, todo,
550 def _get_address_invoice(self, cursor, user, picking):
551 '''Return {'contact': address, 'invoice': address} for invoice'''
552 partner_obj = self.pool.get('res.partner')
553 partner = picking.address_id.partner_id
555 return partner_obj.address_get(cursor, user, [partner.id],
556 ['contact', 'invoice'])
558 def _get_comment_invoice(self, cursor, user, picking):
559 '''Return comment string for invoice'''
560 return picking.note or ''
562 def _get_price_unit_invoice(self, cursor, user, move_line, type):
563 '''Return the price unit for the move line'''
564 if type in ('in_invoice', 'in_refund'):
565 return move_line.product_id.standard_price
567 return move_line.product_id.list_price
569 def _get_discount_invoice(self, cursor, user, move_line):
570 '''Return the discount for the move line'''
573 def _get_taxes_invoice(self, cursor, user, move_line, type):
574 '''Return taxes ids for the move line'''
575 if type in ('in_invoice', 'in_refund'):
576 taxes = move_line.product_id.supplier_taxes_id
578 taxes = move_line.product_id.taxes_id
580 if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
581 return self.pool.get('account.fiscal.position').map_tax(
584 move_line.picking_id.address_id.partner_id,
588 return map(lambda x: x.id, taxes)
590 def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
593 def _invoice_line_hook(self, cursor, user, move_line, invoice_line_id):
594 '''Call after the creation of the invoice line'''
597 def _invoice_hook(self, cursor, user, picking, invoice_id):
598 '''Call after the creation of the invoice'''
601 def action_invoice_create(self, cursor, user, ids, journal_id=False,
602 group=False, type='out_invoice', context=None):
603 print "WW"*12,context
604 '''Return ids of created invoices for the pickings'''
605 invoice_obj = self.pool.get('account.invoice')
606 invoice_line_obj = self.pool.get('account.invoice.line')
609 sale_line_obj = self.pool.get('sale.order.line')
611 for picking in self.browse(cursor, user, ids, context=context):
612 if picking.invoice_state != '2binvoiced':
614 payment_term_id = False
615 partner = picking.address_id and picking.address_id.partner_id
617 raise osv.except_osv(_('Error, no partner !'),
618 _('Please put a partner on the picking list if you want to generate invoice.'))
620 if type in ('out_invoice', 'out_refund'):
621 account_id = partner.property_account_receivable.id
622 if picking.sale_id and picking.sale_id.payment_term:
623 payment_term_id= picking.sale_id.payment_term.id
625 account_id = partner.property_account_payable.id
627 address_contact_id, address_invoice_id = \
628 self._get_address_invoice(cursor, user, picking).values()
630 comment = self._get_comment_invoice(cursor, user, picking)
632 if group and partner.id in invoices_group:
633 invoice_id = invoices_group[partner.id]
636 'name': picking.name,
637 'origin': picking.name + (picking.origin and (':' + picking.origin) or ''),
639 'account_id': account_id,
640 'partner_id': partner.id,
641 'address_invoice_id': address_invoice_id,
642 'address_contact_id': address_contact_id,
644 'payment_term': payment_term_id,
647 invoice_vals['journal_id'] = journal_id
648 invoice_id = invoice_obj.create(cursor, user, invoice_vals,
650 invoices_group[partner.id] = invoice_id
651 res[picking.id] = invoice_id
652 sale_line_ids = sale_line_obj.search(cursor, user, [('order_id','=',picking.sale_id.id)])
653 sale_lines = sale_line_obj.browse(cursor, user, sale_line_ids, context=context)
654 for sale_line in sale_lines:
655 if sale_line.product_id.type == 'service' and sale_line.invoiced == False:
657 name = picking.name + '-' + sale_line.name
659 name = sale_line.name
660 if type in ('out_invoice', 'out_refund'):
661 account_id = sale_line.product_id.product_tmpl_id.\
662 property_account_income.id
664 account_id = sale_line.product_id.categ_id.\
665 property_account_income_categ.id
667 account_id = sale_line.product_id.product_tmpl_id.\
668 property_account_expense.id
670 account_id = sale_line.product_id.categ_id.\
671 property_account_expense_categ.id
672 price_unit = self._get_price_unit_invoice(cursor, user,
674 discount = self._get_discount_invoice(cursor, user, sale_line)
675 tax_ids = self._get_taxes_invoice(cursor, user, sale_line, type)
677 account_analytic_id = self._get_account_analytic_invoice(cursor,
678 user, picking, sale_line)
680 account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
681 invoice_line_id = invoice_line_obj.create(cursor, user, {
683 'invoice_id': invoice_id,
684 'uos_id': sale_line.product_uos.id or sale_line.product_uom.id,
685 'product_id': sale_line.product_id.id,
686 'account_id': account_id,
687 'price_unit': price_unit,
688 'discount': discount,
689 'quantity': sale_line.product_uos_qty,
690 'invoice_line_tax_id': [(6, 0, tax_ids)],
691 'account_analytic_id': account_analytic_id,
693 sale_line_obj.write(cursor, user, [sale_line.id], {'invoiced':True,
694 'invoice_lines': [(6, 0, [invoice_line_id])],
697 for move_line in picking.move_lines:
699 name = picking.name + '-' + move_line.name
701 name = move_line.name
703 if type in ('out_invoice', 'out_refund'):
704 account_id = move_line.product_id.product_tmpl_id.\
705 property_account_income.id
707 account_id = move_line.product_id.categ_id.\
708 property_account_income_categ.id
710 account_id = move_line.product_id.product_tmpl_id.\
711 property_account_expense.id
713 account_id = move_line.product_id.categ_id.\
714 property_account_expense_categ.id
716 price_unit = self._get_price_unit_invoice(cursor, user,
718 discount = self._get_discount_invoice(cursor, user, move_line)
719 tax_ids = self._get_taxes_invoice(cursor, user, move_line, type)
720 account_analytic_id = self._get_account_analytic_invoice(cursor,
721 user, picking, move_line)
723 account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
724 invoice_line_id = invoice_line_obj.create(cursor, user, {
726 'invoice_id': invoice_id,
727 'uos_id': move_line.product_uos.id,
728 'product_id': move_line.product_id.id,
729 'account_id': account_id,
730 'price_unit': price_unit,
731 'discount': discount,
732 'quantity': move_line.product_uos_qty or move_line.product_qty,
733 'invoice_line_tax_id': [(6, 0, tax_ids)],
734 'account_analytic_id': account_analytic_id,
736 self._invoice_line_hook(cursor, user, move_line, invoice_line_id)
738 invoice_obj.button_compute(cursor, user, [invoice_id], context=context,
739 set_total=(type in ('in_invoice', 'in_refund')))
740 self.write(cursor, user, [picking.id], {
741 'invoice_state': 'invoiced',
743 self._invoice_hook(cursor, user, picking, invoice_id)
744 self.write(cursor, user, res.keys(), {
745 'invoice_state': 'invoiced',
752 class stock_production_lot(osv.osv):
754 def name_get(self, cr, uid, ids, context={}):
757 reads = self.read(cr, uid, ids, ['name', 'ref'], context)
762 name=name+'/'+record['ref']
763 res.append((record['id'], name))
767 _name = 'stock.production.lot'
768 _description = 'Production lot'
770 def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
771 if 'location_id' not in context:
772 locations = self.pool.get('stock.location').search(cr, uid, [('usage','=','internal')], context=context)
774 locations = [context['location_id']]
775 res = {}.fromkeys(ids, 0.0)
780 stock_report_prodlots
782 location_id in ('''+','.join(map(str, locations))+''') and
783 prodlot_id in ('''+','.join(map(str, ids))+''')
787 res.update(dict(cr.fetchall()))
791 'name': fields.char('Serial', size=64, required=True),
792 'ref': fields.char('Internal Ref.', size=64),
793 'product_id': fields.many2one('product.product','Product',required=True),
794 'date': fields.datetime('Created Date', required=True),
795 'stock_available': fields.function(_get_stock, method=True, type="float", string="Available", select="2"),
796 'revisions': fields.one2many('stock.production.lot.revision','lot_id','Revisions'),
799 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
800 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.lot.serial'),
801 'product_id': lambda x,y,z,c: c.get('product_id',False),
804 ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
807 stock_production_lot()
809 class stock_production_lot_revision(osv.osv):
810 _name = 'stock.production.lot.revision'
811 _description = 'Production lot revisions'
813 'name': fields.char('Revision name', size=64, required=True),
814 'description': fields.text('Description'),
815 'date': fields.date('Revision date'),
816 'indice': fields.char('Revision', size=16),
817 'author_id': fields.many2one('res.users', 'Author'),
818 'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
822 'author_id': lambda x,y,z,c: z,
823 'date': lambda *a: time.strftime('%Y-%m-%d'),
825 stock_production_lot_revision()
827 # ----------------------------------------------------
829 # ----------------------------------------------------
833 # location_dest_id is only used for predicting futur stocks
835 class stock_move(osv.osv):
836 def _getSSCC(self, cr, uid, context={}):
837 cr.execute('select id from stock_tracking where create_uid=%d order by id desc limit 1', (uid,))
839 return (res and res[0]) or False
841 _description = "Stock Move"
842 def name_get(self, cr, uid, ids, context={}):
844 for line in self.browse(cr, uid, ids, context):
845 res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
848 def _check_tracking(self, cr, uid, ids):
849 for move in self.browse(cr, uid, ids):
850 if not move.prodlot_id and \
851 (move.state == 'done' and \
853 (move.product_id.track_production and move.location_id.usage=='production') or \
854 (move.product_id.track_production and move.location_dest_id.usage=='production') or \
855 (move.product_id.track_incoming and move.location_id.usage=='supplier') or \
856 (move.product_id.track_outgoing and move.location_dest_id.usage=='customer') \
861 def _check_product_lot(self, cr, uid, ids):
862 for move in self.browse(cr, uid, ids):
863 if move.prodlot_id and (move.prodlot_id.product_id.id != move.product_id.id):
868 'name': fields.char('Name', size=64, required=True, select=True),
869 'priority': fields.selection([('0','Not urgent'),('1','Urgent')], 'Priority'),
871 'date': fields.datetime('Date Created'),
872 'date_planned': fields.datetime('Scheduled date', required=True),
874 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
876 'product_qty': fields.float('Quantity', required=True),
877 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
878 'product_uos_qty': fields.float('Quantity (UOS)'),
879 'product_uos': fields.many2one('product.uom', 'Product UOS'),
880 'product_packaging' : fields.many2one('product.packaging', 'Packaging'),
882 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
883 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
884 'address_id' : fields.many2one('res.partner.address', 'Dest. Address'),
886 'prodlot_id' : fields.many2one('stock.production.lot', 'Production lot', help="Production lot is used to put a serial number on the production"),
887 'tracking_id': fields.many2one('stock.tracking', 'Tracking lot', select=True, help="Tracking lot is the code that will be put on the logistic unit/pallet"),
888 # 'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
890 'auto_validate': fields.boolean('Auto Validate'),
892 'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
893 'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
894 'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
895 'picking_id': fields.many2one('stock.picking', 'Packing list', select=True),
897 'note': fields.text('Notes'),
899 'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'Status', readonly=True, select=True),
900 'price_unit': fields.float('Unit Price',
901 digits=(16, int(config['price_accuracy']))),
905 'You must assign a production lot for this product',
908 'You try to assign a lot which is not from the same product',
910 def _default_location_destination(self, cr, uid, context={}):
911 if context.get('move_line', []):
912 return context['move_line'][0][2]['location_dest_id']
913 if context.get('address_out_id', False):
914 return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
917 def _default_location_source(self, cr, uid, context={}):
918 if context.get('move_line', []):
920 return context['move_line'][0][2]['location_id']
923 if context.get('address_in_id', False):
924 return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
928 'location_id': _default_location_source,
929 'location_dest_id': _default_location_destination,
930 'state': lambda *a: 'draft',
931 'priority': lambda *a: '1',
932 'product_qty': lambda *a: 1.0,
933 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
934 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
937 def _auto_init(self, cursor, context):
938 super(stock_move, self)._auto_init(cursor, context)
939 cursor.execute('SELECT indexname \
941 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
942 if not cursor.fetchone():
943 cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
944 ON stock_move (location_id, location_dest_id, product_id, state)')
947 def onchange_lot_id(self, cr, uid, context, prodlot_id=False,product_qty=False, loc_id=False):
948 if not prodlot_id or not loc_id:
950 prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id)
951 location=self.pool.get('stock.location').browse(cr,uid,loc_id)
953 if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
955 'title':'Bad Lot Assignation !',
956 'message':'You are moving %.2f products but only %.2f available in this lot.' % (product_qty,prodlot.stock_available or 0.0)
958 return {'warning':warning}
960 def onchange_product_id(self, cr, uid, context, prod_id=False, loc_id=False, loc_dest_id=False):
963 product = self.pool.get('product.product').browse(cr, uid, [prod_id])[0]
965 'name': product.name,
966 'product_uom': product.uom_id.id,
969 result['location_id'] = loc_id
971 result['location_dest_id'] = loc_dest_id
972 return {'value':result}
974 def _chain_compute(self, cr, uid, moves, context={}):
977 dest = self.pool.get('stock.location').chained_location_get(
981 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
986 if dest[1]=='transparent':
987 self.write(cr, uid, [m.id], {
988 'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
989 DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
990 'location_dest_id': dest[0].id})
992 result.setdefault(m.picking_id, [])
993 result[m.picking_id].append( (m, dest) )
996 def action_confirm(self, cr, uid, moves, context={}):
997 ids = map(lambda m: m.id, moves)
998 self.write(cr, uid, ids, {'state':'confirmed'})
999 for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1000 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1001 pickid = self.pool.get('stock.picking').create(cr, uid, {
1002 'name': picking.name,
1003 'origin': str(picking.origin or ''),
1005 'note': picking.note,
1006 'move_type': picking.move_type,
1007 'auto_picking': todo[0][1][1]=='auto',
1008 'address_id': picking.address_id.id,
1009 'invoice_state': 'none'
1011 for move,(loc,auto,delay) in todo:
1012 # Is it smart to copy ? May be it's better to recreate ?
1013 new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
1014 'location_id': move.location_dest_id.id,
1015 'location_dest_id': loc.id,
1016 'date_moved': time.strftime('%Y-%m-%d'),
1017 'picking_id': pickid,
1019 'move_history_ids':[],
1020 'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
1021 'move_history_ids2':[]}
1023 self.pool.get('stock.move').write(cr, uid, [move.id], {
1024 'move_dest_id': new_id,
1025 'move_history_ids': [(4, new_id)]
1027 wf_service = netsvc.LocalService("workflow")
1028 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1031 def action_assign(self, cr, uid, ids, *args):
1033 for move in self.browse(cr, uid, ids):
1034 if move.state in ('confirmed','waiting'):
1035 todo.append(move.id)
1036 res = self.check_assign(cr, uid, todo)
1039 def force_assign(self, cr, uid, ids, context={}):
1040 self.write(cr, uid, ids, {'state' : 'assigned'})
1043 def cancel_assign(self, cr, uid, ids, context={}):
1044 self.write(cr, uid, ids, {'state': 'confirmed'})
1048 # Duplicate stock.move
1050 def check_assign(self, cr, uid, ids, context={}):
1054 for move in self.browse(cr, uid, ids):
1055 if move.product_id.type == 'consu':
1056 if move.state in ('confirmed', 'waiting'):
1057 done.append(move.id)
1058 pickings[move.picking_id.id] = 1
1060 if move.state in ('confirmed','waiting'):
1061 res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id})
1063 done.append(move.id)
1064 pickings[move.picking_id.id] = 1
1066 cr.execute('update stock_move set location_id=%d, product_qty=%f where id=%d', (r[1],r[0], move.id))
1070 move_id = self.copy(cr, uid, move.id, {'product_qty':r[0], 'location_id':r[1]})
1071 done.append(move_id)
1072 #cr.execute('insert into stock_move_history_ids values (%d,%d)', (move.id,move_id))
1075 self.write(cr, uid, done, {'state':'assigned'})
1078 for pick_id in pickings:
1079 wf_service = netsvc.LocalService("workflow")
1080 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1084 # Cancel move => cancel others move and pickings
1086 def action_cancel(self, cr, uid, ids, context={}):
1090 for move in self.browse(cr, uid, ids):
1091 if move.state in ('confirmed','waiting','assigned','draft'):
1093 pickings[move.picking_id.id] = True
1094 self.write(cr, uid, ids, {'state':'cancel'})
1096 for pick_id in pickings:
1097 wf_service = netsvc.LocalService("workflow")
1098 wf_service.trg_validate(uid, 'stock.picking', pick_id, 'button_cancel', cr)
1100 for res in self.read(cr, uid, ids, ['move_dest_id']):
1101 if res['move_dest_id']:
1102 ids2.append(res['move_dest_id'][0])
1104 wf_service = netsvc.LocalService("workflow")
1106 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1107 self.action_cancel(cr,uid, ids2, context)
1110 def action_done(self, cr, uid, ids, context=None):
1112 for move in self.browse(cr, uid, ids):
1113 if move.move_dest_id.id and (move.state != 'done'):
1114 mid = move.move_dest_id.id
1115 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, move.move_dest_id.id))
1116 if move.move_dest_id.state in ('waiting','confirmed'):
1117 self.write(cr, uid, [move.move_dest_id.id], {'state':'assigned'})
1118 if move.move_dest_id.picking_id:
1119 wf_service = netsvc.LocalService("workflow")
1120 wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1123 # self.action_done(cr, uid, [move.move_dest_id.id])
1124 if move.move_dest_id.auto_validate:
1125 self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1128 # Accounting Entries
1132 if move.location_id.account_id:
1133 acc_src = move.location_id.account_id.id
1134 if move.location_dest_id.account_id:
1135 acc_dest = move.location_dest_id.account_id.id
1136 if acc_src or acc_dest:
1137 test = [('product.product', move.product_id.id)]
1138 if move.product_id.categ_id:
1139 test.append( ('product.category', move.product_id.categ_id.id) )
1141 acc_src = move.product_id.product_tmpl_id.\
1142 property_stock_account_input.id
1144 acc_src = move.product_id.categ_id.\
1145 property_stock_account_input_categ.id
1147 raise osv.except_osv(_('Error!'),
1148 _('There is no stock input account defined ' \
1149 'for this product: "%s" (id: %d)') % \
1150 (move.product_id.name,
1151 move.product_id.id,))
1153 acc_dest = move.product_id.product_tmpl_id.\
1154 property_stock_account_output.id
1156 acc_dest = move.product_id.categ_id.\
1157 property_stock_account_output_categ.id
1159 raise osv.except_osv(_('Error!'),
1160 _('There is no stock output account defined ' \
1161 'for this product: "%s" (id: %d)') % \
1162 (move.product_id.name,
1163 move.product_id.id,))
1164 if not move.product_id.categ_id.property_stock_journal.id:
1165 raise osv.except_osv(_('Error!'),
1166 _('There is no journal defined '\
1167 'on the product category: "%s" (id: %d)') % \
1168 (move.product_id.categ_id.name,
1169 move.product_id.categ_id.id,))
1170 journal_id = move.product_id.categ_id.property_stock_journal.id
1171 if acc_src != acc_dest:
1172 ref = move.picking_id and move.picking_id.name or False
1174 if move.product_id.cost_method == 'average' and move.price_unit:
1175 amount = move.product_qty * move.price_unit
1177 amount = move.product_qty * move.product_id.standard_price
1179 date = time.strftime('%Y-%m-%d')
1183 'quantity': move.product_qty,
1185 'account_id': acc_src,
1190 'quantity': move.product_qty,
1192 'account_id': acc_dest,
1196 self.pool.get('account.move').create(cr, uid, {
1198 'journal_id': journal_id,
1202 self.write(cr, uid, ids, {'state':'done','date_planned':time.strftime('%Y-%m-%d %H:%M:%S')})
1203 wf_service = netsvc.LocalService("workflow")
1205 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1208 def unlink(self, cr, uid, ids, context=None):
1209 for move in self.browse(cr, uid, ids, context=context):
1210 if move.state != 'draft':
1211 raise osv.except_osv(_('UserError'),
1212 _('You can only delete draft moves.'))
1213 return super(stock_move, self).unlink(
1214 cr, uid, ids, context=context)
1218 class stock_inventory(osv.osv):
1219 _name = "stock.inventory"
1220 _description = "Inventory"
1222 'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
1223 'date': fields.datetime('Date create', required=True, readonly=True, states={'draft':[('readonly',False)]}),
1224 'date_done': fields.datetime('Date done'),
1225 'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft':[('readonly',False)]}),
1226 'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1227 'state': fields.selection( (('draft','Draft'),('done','Done')), 'Status', readonly=True),
1230 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1231 'state': lambda *a: 'draft',
1234 # Update to support tracking
1236 def action_done(self, cr, uid, ids, context=None):
1237 for inv in self.browse(cr,uid,ids):
1240 for line in inv.inventory_line_id:
1241 pid=line.product_id.id
1242 price=line.product_id.standard_price or 0.0
1243 amount=self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1244 change=line.product_qty-amount
1246 location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1248 'name': 'INV:'+str(line.inventory_id.id)+':'+line.inventory_id.name,
1249 'product_id': line.product_id.id,
1250 'product_uom': line.product_uom.id,
1252 'date_planned': inv.date,
1257 'product_qty': change,
1258 'location_id': location_id,
1259 'location_dest_id': line.location_id.id,
1263 'product_qty': -change,
1264 'location_id': line.location_id.id,
1265 'location_dest_id': location_id,
1267 move_ids.append(self.pool.get('stock.move').create(cr, uid, value))
1269 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1271 self.write(cr, uid, [inv.id], {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6,0,move_ids)]})
1274 def action_cancel(self, cr, uid, ids, context={}):
1275 for inv in self.browse(cr,uid,ids):
1276 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1277 self.write(cr, uid, [inv.id], {'state':'draft'})
1282 class stock_inventory_line(osv.osv):
1283 _name = "stock.inventory.line"
1284 _description = "Inventory line"
1286 'inventory_id': fields.many2one('stock.inventory','Inventory', ondelete='cascade', select=True),
1287 'location_id': fields.many2one('stock.location','Location', required=True),
1288 'product_id': fields.many2one('product.product', 'Product', required=True ),
1289 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1290 'product_qty': fields.float('Quantity')
1292 def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
1296 prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
1297 uom = prod.uom_id.id
1298 amount=self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
1299 result = {'product_qty':amount, 'product_uom':uom}
1300 return {'value':result}
1301 stock_inventory_line()
1304 #----------------------------------------------------------
1306 #----------------------------------------------------------
1307 class stock_warehouse(osv.osv):
1308 _name = "stock.warehouse"
1309 _description = "Warehouse"
1311 'name': fields.char('Name', size=60, required=True),
1312 # 'partner_id': fields.many2one('res.partner', 'Owner'),
1313 'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
1314 'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True ),
1315 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True ),
1316 'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True ),
1322 # get confirm or assign stock move lines of partner and put in current picking.
1323 class stock_picking_move_wizard(osv.osv_memory):
1324 _name='stock.picking.move.wizard'
1325 def _get_picking(self,cr, uid, ctx):
1326 if ctx.get('action_id',False):
1327 return ctx['action_id']
1329 def _get_picking_address(self,cr,uid,ctx):
1330 picking_obj=self.pool.get('stock.picking')
1331 if ctx.get('action_id',False):
1332 picking=picking_obj.browse(cr,uid,[ctx['action_id']])[0]
1333 return picking.address_id and picking.address_id.id or False
1338 'name':fields.char('Name',size=64,invisible=True),
1339 #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
1340 'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines',required=True),
1341 'address_id' : fields.many2one('res.partner.address', 'Dest. Address',invisible=True),
1342 'picking_id': fields.many2one('stock.picking', 'Packing list', select=True,invisible=True),
1345 'picking_id':_get_picking,
1346 'address_id':_get_picking_address,
1348 def action_move(self,cr,uid,ids,context=None):
1349 move_obj=self.pool.get('stock.move')
1350 picking_obj=self.pool.get('stock.picking')
1351 for act in self.read(cr,uid,ids):
1352 move_lines=move_obj.browse(cr,uid,act['move_ids'])
1353 for line in move_lines:
1354 picking_obj.write(cr,uid,[line.picking_id.id],{'move_lines':[(1,line.id,{'picking_id':act['picking_id']})]})
1355 picking_obj.write(cr,uid,[act['picking_id']],{'move_lines':[(1,line.id,{'picking_id':act['picking_id']})]})
1357 old_picking=picking_obj.read(cr,uid,[line.picking_id.id])[0]
1358 if not len(old_picking['move_lines']):
1359 picking_obj.write(cr,uid,[old_picking['id']],{'state':'done'})
1360 return {'type':'ir.actions.act_window_close' }
1362 stock_picking_move_wizard()
1363 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: