1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from mx import DateTime
26 from osv import fields, osv
27 from tools import config
28 from tools.translate import _
33 #----------------------------------------------------------
35 #----------------------------------------------------------
36 class stock_incoterms(osv.osv):
37 _name = "stock.incoterms"
38 _description = "Incoterms"
40 'name': fields.char('Name', size=64, required=True),
41 'code': fields.char('Code', size=3, required=True),
42 'active': fields.boolean('Active'),
45 'active': lambda *a: True,
51 #----------------------------------------------------------
53 #----------------------------------------------------------
54 class stock_location(osv.osv):
55 _name = "stock.location"
56 _description = "Location"
57 _parent_name = "location_id"
60 _order = 'parent_left'
62 def _complete_name(self, cr, uid, ids, name, args, context):
63 def _get_one_full_name(location, level=4):
64 if location.location_id:
65 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
68 return parent_path + location.name
70 for m in self.browse(cr, uid, ids, context=context):
71 res[m.id] = _get_one_full_name(m)
74 def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
77 res[id] = {}.fromkeys(field_names, 0.0)
78 if ('product_id' not in context) or not ids:
80 #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
82 context['location'] = [loc]
83 prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
84 if 'stock_real' in field_names:
85 res[loc]['stock_real'] = prod.qty_available
86 if 'stock_virtual' in field_names:
87 res[loc]['stock_virtual'] = prod.virtual_available
90 def product_detail(self, cr, uid, id, field, context={}):
94 field_to_read = 'virtual_available'
95 if field == 'stock_real_value':
96 field_to_read = 'qty_available'
97 cr.execute('select distinct product_id from stock_move where (location_id=%s) or (location_dest_id=%s)', (id, id))
98 result = cr.dictfetchall()
101 c = (context or {}).copy()
103 product = self.pool.get('product.product').read(cr, uid, r['product_id'], [field_to_read, 'standard_price'], context=c)
104 final_value += (product[field_to_read] * product['standard_price'])
107 def _product_value(self, cr, uid, ids, field_names, arg, context={}):
110 result[id] = {}.fromkeys(field_names, 0.0)
111 for field_name in field_names:
113 ret_dict = self.product_detail(cr, uid, loc, field=field_name)
114 result[loc][field_name] = ret_dict
118 'name': fields.char('Location Name', size=64, required=True, translate=True),
119 'active': fields.boolean('Active'),
120 'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production')], 'Location Type', required=True),
121 'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True),
123 'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
125 'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
126 'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
128 'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type', '!=', 'view')]),
129 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
130 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
132 'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
133 'chained_location_type': fields.selection([('none', 'None'), ('customer', 'Customer'), ('fixed', 'Fixed Location')],
134 'Chained Location Type', required=True),
135 'chained_auto_packing': fields.selection(
136 [('auto', 'Automatic Move'), ('manual', 'Manual Operation'), ('transparent', 'Automatic No Step Added')],
139 help="This is used only if you selected a chained location type.\n" \
140 "The 'Automatic Move' value will create a stock move after the current one that will be "\
141 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
142 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
144 'chained_delay': fields.integer('Chained Delay (days)'),
145 'address_id': fields.many2one('res.partner.address', 'Location Address'),
146 'icon': fields.selection(tools.icons, 'Icon', size=64),
148 'comment': fields.text('Additional Information'),
149 'posx': fields.integer('Corridor (X)'),
150 'posy': fields.integer('Shelves (Y)'),
151 'posz': fields.integer('Height (Z)'),
153 'parent_left': fields.integer('Left Parent', select=1),
154 'parent_right': fields.integer('Right Parent', select=1),
155 'stock_real_value': fields.function(_product_value, method=True, type='float', string='Real Stock Value', multi="stock"),
156 'stock_virtual_value': fields.function(_product_value, method=True, type='float', string='Virtual Stock Value', multi="stock"),
159 'active': lambda *a: 1,
160 'usage': lambda *a: 'internal',
161 'allocation_method': lambda *a: 'fifo',
162 'chained_location_type': lambda *a: 'none',
163 'chained_auto_packing': lambda *a: 'manual',
164 'posx': lambda *a: 0,
165 'posy': lambda *a: 0,
166 'posz': lambda *a: 0,
167 'icon': lambda *a: False
170 def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
172 if location.chained_location_type == 'customer':
174 result = partner.property_stock_customer
175 elif location.chained_location_type == 'fixed':
176 result = location.chained_location_id
178 return result, location.chained_auto_packing, location.chained_delay
181 def picking_type_get(self, cr, uid, from_location, to_location, context={}):
183 if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer', 'supplier')):
185 elif (from_location.usage in ('supplier', 'customer')) and (to_location.usage=='internal'):
189 def _product_get_all_report(self, cr, uid, ids, product_ids=False,
191 return self._product_get_report(cr, uid, ids, product_ids, context,
194 def _product_get_report(self, cr, uid, ids, product_ids=False,
195 context=None, recursive=False):
198 product_obj = self.pool.get('product.product')
200 product_ids = product_obj.search(cr, uid, [])
202 products = product_obj.browse(cr, uid, product_ids, context=context)
205 for product in products:
206 products_by_uom.setdefault(product.uom_id.id, [])
207 products_by_uom[product.uom_id.id].append(product)
208 products_by_id.setdefault(product.id, [])
209 products_by_id[product.id] = product
212 result['product'] = []
216 for uom_id in products_by_uom.keys():
217 fnc = self._product_get
219 fnc = self._product_all_get
222 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
224 for product_id in qty.keys():
225 if not qty[product_id]:
227 product = products_by_id[product_id]
228 quantity_total += qty[product_id]
229 price = qty[product_id] * product.standard_price
231 result['product'].append({
232 'price': product.standard_price,
233 'prod_name': product.name,
234 'code': product.default_code, # used by lot_overview_all report!
235 'variants': product.variants or '',
236 'uom': product.uom_id.name,
237 'prod_qty': qty[product_id],
238 'price_value': price,
240 result['total'] = quantity_total
241 result['total_price'] = total_price
244 def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):
245 product_obj = self.pool.get('product.product')
251 return product_obj.get_product_available(cr, uid, product_ids, context=context)
253 def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
254 ids = id and [id] or []
255 context.update({'compute_child':False})
256 return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
258 def _product_all_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
259 # build the list of ids of children of the location given by id
260 ids = id and [id] or []
261 # location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
262 return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
264 def _product_virtual_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
265 return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
268 def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
270 Attempt to find a quantity ``product_qty`` (in the product's default uom or the uom passed in ``context``) of product ``product_id``
271 in locations with id ``ids`` and their child locations. If ``lock`` is True, the stock.move lines
272 of product with id ``product_id`` in the searched location will be write-locked using Postgres's
273 "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reservin
274 twice the same products.
275 If ``lock`` is True and the lock cannot be obtained (because another transaction has locked some of
276 the same stock.move lines), a log line will be output and False will be returned, as if there was
279 :param product_id: Id of product to reserve
280 :param product_qty: Quantity of product to reserve (in the product's default uom or the uom passed in ``context``)
281 :param lock: if True, the stock.move lines of product with id ``product_id`` in all locations (and children locations) with ``ids`` will
282 be write-locked using postgres's "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back. This is
283 to prevent reserving twice the same products.
284 :param context: optional context dictionary: it a 'uom' key is present it will be used instead of the default product uom to
285 compute the ``product_qty`` and in the return value.
286 :return: List of tuples in the form (qty, location_id) with the (partial) quantities that can be taken in each location to
287 reach the requested product_qty (``qty`` is expressed in the default uom of the product), of False if enough
288 products could not be found, or the lock could not be obtained (and ``lock`` was True).
294 for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
297 # Must lock with a separate select query because FOR UPDATE can't be used with
298 # aggregation/group by's (when individual rows aren't identifiable).
299 # We use a SAVEPOINT to be able to rollback this part of the transaction without
300 # failing the whole transaction in case the LOCK cannot be acquired.
301 cr.execute("SAVEPOINT stock_location_product_reserve")
302 cr.execute("""SELECT id FROM stock_move
303 WHERE product_id=%s AND
305 (location_dest_id=%s AND
310 location_dest_id<>%s AND
311 state in ('done', 'assigned'))
313 FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id))
315 # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
316 # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
317 # state, we return False as if the products were not available, and log it:
318 cr.execute("ROLLBACK TO stock_location_product_reserve")
319 logger = logging.getLogger('stock.location')
320 logger.warn("Failed attempt to reserve %s x product %s, likely due to another transaction already in progress. Next attempt is likely to work. Detailed error available at DEBUG level.", product_qty, product_id)
321 logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
324 # XXX TODO: rewrite this with one single query, possibly even the quantity conversion
325 cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
327 WHERE location_dest_id=%s AND
333 (id, id, product_id))
334 results = cr.dictfetchall()
335 cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
337 WHERE location_id=%s AND
338 location_dest_id<>%s AND
340 state in ('done', 'assigned')
343 (id, id, product_id))
344 results += cr.dictfetchall()
349 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
358 if amount > min(total, product_qty):
359 amount = min(product_qty, total)
360 result.append((amount, id))
361 product_qty -= amount
363 if product_qty <= 0.0:
372 class stock_tracking(osv.osv):
373 _name = "stock.tracking"
374 _description = "Stock Tracking Lots"
377 salt = '31' * 8 + '3'
379 for sscc_part, salt_part in zip(sscc, salt):
380 sum += int(sscc_part) * int(salt_part)
381 return (10 - (sum % 10)) % 10
382 checksum = staticmethod(checksum)
384 def make_sscc(self, cr, uid, context={}):
385 sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
386 return sequence + str(self.checksum(sequence))
389 'name': fields.char('Tracking', size=64, required=True),
390 'active': fields.boolean('Active'),
391 'serial': fields.char('Reference', size=64),
392 'move_ids': fields.one2many('stock.move', 'tracking_id', 'Moves Tracked'),
393 'date': fields.datetime('Date Created', required=True),
396 'active': lambda *a: 1,
398 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
401 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
406 ids = self.search(cr, user, [('serial', '=', name)]+ args, limit=limit, context=context)
407 ids += self.search(cr, user, [('name', operator, name)]+ args, limit=limit, context=context)
408 return self.name_get(cr, user, ids, context)
410 def name_get(self, cr, uid, ids, context={}):
413 res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name', 'serial'], context)]
416 def unlink(self, cr, uid, ids, context=None):
417 raise osv.except_osv(_('Error'), _('You can not remove a lot line !'))
422 #----------------------------------------------------------
424 #----------------------------------------------------------
425 class stock_picking(osv.osv):
426 _name = "stock.picking"
427 _description = "Packing List"
429 def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
432 if isinstance(ids, (int, long)):
434 for pick in self.browse(cr, uid, ids, context):
435 sql_str = """update stock_move set
439 sqlargs = (value, pick.id)
442 sql_str += " and (date_planned=%s or date_planned>%s)"
443 sqlargs += (pick.max_date, value)
444 cr.execute(sql_str, sqlargs)
447 def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
450 if isinstance(ids, (int, long)):
452 for pick in self.browse(cr, uid, ids, context):
453 sql_str = """update stock_move set
457 sqlargs = (value, pick.id)
459 sql_str += " and (date_planned=%s or date_planned<%s)"
460 sqlargs += (pick.min_date, value)
461 cr.execute(sql_str, sqlargs)
464 def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
467 res[id] = {'min_date': False, 'max_date': False}
479 picking_id""", (tuple(ids),))
480 for pick, dt1, dt2 in cr.fetchall():
481 res[pick]['min_date'] = dt1
482 res[pick]['max_date'] = dt2
485 def create(self, cr, user, vals, context=None):
486 if ('name' not in vals) or (vals.get('name')=='/'):
487 vals['name'] = self.pool.get('ir.sequence').get(cr, user, 'stock.picking')
489 return super(stock_picking, self).create(cr, user, vals, context)
492 'name': fields.char('Reference', size=64, select=True),
493 'origin': fields.char('Origin Reference', size=64),
494 'backorder_id': fields.many2one('stock.picking', 'Back Order'),
495 'type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal'), ('delivery', 'Delivery')], 'Shipping Type', required=True, select=True),
496 'active': fields.boolean('Active'),
497 'note': fields.text('Notes'),
499 'location_id': fields.many2one('stock.location', 'Location'),
500 'location_dest_id': fields.many2one('stock.location', 'Dest. Location'),
501 'move_type': fields.selection([('direct', 'Direct Delivery'), ('one', 'All at once')], 'Delivery Method', required=True),
502 'state': fields.selection([
505 ('confirmed', 'Confirmed'),
506 ('assigned', 'Available'),
508 ('cancel', 'Cancelled'),
509 ], 'Status', readonly=True, select=True),
510 'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
511 method=True, store=True, type='datetime', string='Planned Date', select=1),
512 'date': fields.datetime('Date Order'),
513 'date_done': fields.datetime('Date Done'),
514 'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
515 method=True, store=True, type='datetime', string='Max. Planned Date', select=2),
516 'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines', states={'cancel': [('readonly', True)]}),
517 'auto_picking': fields.boolean('Auto-Packing'),
518 'address_id': fields.many2one('res.partner.address', 'Partner'),
519 'invoice_state': fields.selection([
520 ("invoiced", "Invoiced"),
521 ("2binvoiced", "To Be Invoiced"),
522 ("none", "Not from Packing")], "Invoice Status",
523 select=True, required=True, readonly=True, states={'draft': [('readonly', False)]}),
526 'name': lambda self, cr, uid, context: '/',
527 'active': lambda *a: 1,
528 'state': lambda *a: 'draft',
529 'move_type': lambda *a: 'direct',
530 'type': lambda *a: 'in',
531 'invoice_state': lambda *a: 'none',
532 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
535 def copy(self, cr, uid, id, default=None, context={}):
538 default = default.copy()
539 if not default.get('name',False):
540 default['name'] = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking')
541 return super(stock_picking, self).copy(cr, uid, id, default, context)
543 def onchange_partner_in(self, cr, uid, context, partner_id=None):
546 def action_explode(self, cr, uid, moves, context={}):
549 def action_confirm(self, cr, uid, ids, context={}):
550 self.write(cr, uid, ids, {'state': 'confirmed'})
552 for picking in self.browse(cr, uid, ids):
553 for r in picking.move_lines:
554 if r.state == 'draft':
556 todo = self.action_explode(cr, uid, todo, context)
558 self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
561 def test_auto_picking(self, cr, uid, ids):
562 # TODO: Check locations to see if in the same location ?
565 def button_confirm(self, cr, uid, ids, *args):
567 wf_service = netsvc.LocalService("workflow")
568 wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
569 self.force_assign(cr, uid, ids, *args)
572 def action_assign(self, cr, uid, ids, *args):
573 for pick in self.browse(cr, uid, ids):
574 move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
575 self.pool.get('stock.move').action_assign(cr, uid, move_ids)
578 def force_assign(self, cr, uid, ids, *args):
579 wf_service = netsvc.LocalService("workflow")
580 for pick in self.browse(cr, uid, ids):
581 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','waiting']]
582 # move_ids = [x.id for x in pick.move_lines]
583 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
584 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
587 def draft_force_assign(self, cr, uid, ids, *args):
588 wf_service = netsvc.LocalService("workflow")
589 for pick in self.browse(cr, uid, ids):
590 wf_service.trg_validate(uid, 'stock.picking', pick.id,
591 'button_confirm', cr)
592 #move_ids = [x.id for x in pick.move_lines]
593 #self.pool.get('stock.move').force_assign(cr, uid, move_ids)
594 #wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
597 def draft_validate(self, cr, uid, ids, *args):
598 wf_service = netsvc.LocalService("workflow")
599 self.draft_force_assign(cr, uid, ids)
600 for pick in self.browse(cr, uid, ids):
601 move_ids = [x.id for x in pick.move_lines]
602 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
603 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
605 self.action_move(cr, uid, [pick.id])
606 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
609 def cancel_assign(self, cr, uid, ids, *args):
610 wf_service = netsvc.LocalService("workflow")
611 for pick in self.browse(cr, uid, ids):
612 move_ids = [x.id for x in pick.move_lines]
613 self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
614 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
617 def action_assign_wkf(self, cr, uid, ids):
618 self.write(cr, uid, ids, {'state': 'assigned'})
621 def test_finnished(self, cr, uid, ids):
622 move_ids = self.pool.get('stock.move').search(cr, uid, [('picking_id', 'in', ids)])
623 for move in self.pool.get('stock.move').browse(cr, uid, move_ids):
624 if move.state not in ('done', 'cancel'):
625 if move.product_qty != 0.0:
628 move.write(cr, uid, [move.id], {'state': 'done'})
631 def test_assigned(self, cr, uid, ids):
633 for pick in self.browse(cr, uid, ids):
635 for move in pick.move_lines:
636 if (move.state in ('confirmed', 'draft')) and (mt=='one'):
638 if (mt=='direct') and (move.state=='assigned') and (move.product_qty):
640 ok = ok and (move.state in ('cancel', 'done', 'assigned'))
643 def action_cancel(self, cr, uid, ids, context={}):
644 for pick in self.browse(cr, uid, ids):
645 ids2 = [move.id for move in pick.move_lines]
646 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
647 self.write(cr, uid, ids, {'state': 'cancel', 'invoice_state': 'none'})
651 # TODO: change and create a move if not parents
653 def action_done(self, cr, uid, ids, context=None):
654 self.write(cr, uid, ids, {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
657 def action_move(self, cr, uid, ids, context={}):
658 for pick in self.browse(cr, uid, ids):
660 for move in pick.move_lines:
661 if move.state == 'assigned':
665 self.pool.get('stock.move').action_done(cr, uid, todo,
669 def get_currency_id(self, cursor, user, picking):
672 def _get_payment_term(self, cursor, user, picking):
673 '''Return {'contact': address, 'invoice': address} for invoice'''
674 partner_obj = self.pool.get('res.partner')
675 partner = picking.address_id.partner_id
676 return partner.property_payment_term and partner.property_payment_term.id or False
678 def _get_address_invoice(self, cursor, user, picking):
679 '''Return {'contact': address, 'invoice': address} for invoice'''
680 partner_obj = self.pool.get('res.partner')
681 partner = picking.address_id.partner_id
683 return partner_obj.address_get(cursor, user, [partner.id],
684 ['contact', 'invoice'])
686 def _get_comment_invoice(self, cursor, user, picking):
687 '''Return comment string for invoice'''
688 return picking.note or ''
690 def _get_price_unit_invoice(self, cursor, user, move_line, type):
691 '''Return the price unit for the move line'''
692 if type in ('in_invoice', 'in_refund'):
693 return move_line.product_id.standard_price
695 return move_line.product_id.list_price
697 def _get_discount_invoice(self, cursor, user, move_line):
698 '''Return the discount for the move line'''
701 def _get_taxes_invoice(self, cursor, user, move_line, type):
702 '''Return taxes ids for the move line'''
703 if type in ('in_invoice', 'in_refund'):
704 taxes = move_line.product_id.supplier_taxes_id
706 taxes = move_line.product_id.taxes_id
708 if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
709 return self.pool.get('account.fiscal.position').map_tax(
712 move_line.picking_id.address_id.partner_id.property_account_position,
716 return map(lambda x: x.id, taxes)
718 def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
721 def _invoice_line_hook(self, cursor, user, move_line, invoice_line_id):
722 '''Call after the creation of the invoice line'''
725 def _invoice_hook(self, cursor, user, picking, invoice_id):
726 '''Call after the creation of the invoice'''
729 def action_invoice_create(self, cursor, user, ids, journal_id=False,
730 group=False, type='out_invoice', context=None):
731 '''Return ids of created invoices for the pickings'''
732 invoice_obj = self.pool.get('account.invoice')
733 invoice_line_obj = self.pool.get('account.invoice.line')
737 for picking in self.browse(cursor, user, ids, context=context):
738 if picking.invoice_state != '2binvoiced':
740 payment_term_id = False
741 partner = picking.address_id and picking.address_id.partner_id
743 raise osv.except_osv(_('Error, no partner !'),
744 _('Please put a partner on the picking list if you want to generate invoice.'))
746 if type in ('out_invoice', 'out_refund'):
747 account_id = partner.property_account_receivable.id
748 payment_term_id = self._get_payment_term(cursor, user, picking)
750 account_id = partner.property_account_payable.id
752 address_contact_id, address_invoice_id = \
753 self._get_address_invoice(cursor, user, picking).values()
755 comment = self._get_comment_invoice(cursor, user, picking)
756 if group and partner.id in invoices_group:
757 invoice_id = invoices_group[partner.id]
758 invoice = invoice_obj.browse(cursor, user, invoice_id)
760 'name': (invoice.name or '') + ', ' + (picking.name or ''),
761 'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
762 'comment': (comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
764 invoice_obj.write(cursor, user, [invoice_id], invoice_vals, context=context)
767 'name': picking.name,
768 'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
770 'account_id': account_id,
771 'partner_id': partner.id,
772 'address_invoice_id': address_invoice_id,
773 'address_contact_id': address_contact_id,
775 'payment_term': payment_term_id,
776 'fiscal_position': partner.property_account_position.id
778 cur_id = self.get_currency_id(cursor, user, picking)
780 invoice_vals['currency_id'] = cur_id
782 invoice_vals['journal_id'] = journal_id
783 invoice_id = invoice_obj.create(cursor, user, invoice_vals,
785 invoices_group[partner.id] = invoice_id
786 res[picking.id] = invoice_id
787 for move_line in picking.move_lines:
788 if move_line.state == 'cancel':
790 origin = move_line.picking_id.name
791 if move_line.picking_id.origin:
792 origin += ':' + move_line.picking_id.origin
794 name = (picking.name or '') + '-' + move_line.name
796 name = move_line.name
798 if type in ('out_invoice', 'out_refund'):
799 account_id = move_line.product_id.product_tmpl_id.\
800 property_account_income.id
802 account_id = move_line.product_id.categ_id.\
803 property_account_income_categ.id
805 account_id = move_line.product_id.product_tmpl_id.\
806 property_account_expense.id
808 account_id = move_line.product_id.categ_id.\
809 property_account_expense_categ.id
811 price_unit = self._get_price_unit_invoice(cursor, user,
813 discount = self._get_discount_invoice(cursor, user, move_line)
814 tax_ids = self._get_taxes_invoice(cursor, user, move_line, type)
815 account_analytic_id = self._get_account_analytic_invoice(cursor,
816 user, picking, move_line)
818 #set UoS if it's a sale and the picking doesn't have one
819 uos_id = move_line.product_uos and move_line.product_uos.id or False
820 if not uos_id and type in ('out_invoice', 'out_refund'):
821 uos_id = move_line.product_uom.id
823 account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner.property_account_position, account_id)
824 invoice_line_id = invoice_line_obj.create(cursor, user, {
827 'invoice_id': invoice_id,
829 'product_id': move_line.product_id.id,
830 'account_id': account_id,
831 'price_unit': price_unit,
832 'discount': discount,
833 'quantity': move_line.product_uos_qty or move_line.product_qty,
834 'invoice_line_tax_id': [(6, 0, tax_ids)],
835 'account_analytic_id': account_analytic_id,
837 self._invoice_line_hook(cursor, user, move_line, invoice_line_id)
839 invoice_obj.button_compute(cursor, user, [invoice_id], context=context,
840 set_total=(type in ('in_invoice', 'in_refund')))
841 self.write(cursor, user, [picking.id], {
842 'invoice_state': 'invoiced',
844 self._invoice_hook(cursor, user, picking, invoice_id)
845 self.write(cursor, user, res.keys(), {
846 'invoice_state': 'invoiced',
850 def test_cancel(self, cr, uid, ids, context={}):
851 for pick in self.browse(cr, uid, ids, context=context):
852 for move in pick.move_lines:
853 if move.state not in ('cancel',):
857 def unlink(self, cr, uid, ids, context=None):
858 move_obj = self.pool.get('stock.move')
861 for pick in self.browse(cr, uid, ids, context=context):
862 if pick.state in ['done','cancel']:
863 raise osv.except_osv(_('Error'), _('You cannot remove the picking which is in %s state !')%(pick.state,))
864 elif pick.state in ['confirmed','assigned', 'draft']:
865 ids2 = [move.id for move in pick.move_lines]
867 ctx.update({'call_unlink':True})
868 if pick.state != 'draft':
869 #Cancelling the move in order to affect Virtual stock of product
870 move_obj.action_cancel(cr, uid, ids2, ctx)
872 move_obj.unlink(cr, uid, ids2, ctx)
874 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
879 class stock_production_lot(osv.osv):
880 def name_get(self, cr, uid, ids, context={}):
883 reads = self.read(cr, uid, ids, ['name', 'ref'], context)
886 name = record['name']
888 name = name + '/' + record['ref']
889 res.append((record['id'], name))
892 _name = 'stock.production.lot'
893 _description = 'Production lot'
895 def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
896 if 'location_id' not in context:
897 locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
899 locations = context['location_id'] and [context['location_id']] or []
901 if isinstance(ids, (int, long)):
904 res = {}.fromkeys(ids, 0.0)
911 stock_report_prodlots
913 location_id in %s and
917 ''', (tuple(locations), tuple(ids)))
918 res.update(dict(cr.fetchall()))
921 def _stock_search(self, cr, uid, obj, name, args, context):
922 locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')])
927 stock_report_prodlots
932 having sum(name) ''' + str(args[0][1]) + ' %s',
933 (tuple(locations), args[0][2]))
935 ids = [('id', 'in', map(lambda x: x[0], res))]
939 'name': fields.char('Serial', size=64, required=True),
940 'ref': fields.char('Internal Ref', size=64),
941 'product_id': fields.many2one('product.product', 'Product', required=True),
942 'date': fields.datetime('Created Date', required=True),
943 'stock_available': fields.function(_get_stock, fnct_search=_stock_search, method=True, type="float", string="Available", select="2"),
944 'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'),
947 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
948 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
949 'product_id': lambda x, y, z, c: c.get('product_id', False),
952 ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
955 stock_production_lot()
958 class stock_production_lot_revision(osv.osv):
959 _name = 'stock.production.lot.revision'
960 _description = 'Production lot revisions'
962 'name': fields.char('Revision Name', size=64, required=True),
963 'description': fields.text('Description'),
964 'date': fields.date('Revision Date'),
965 'indice': fields.char('Revision', size=16),
966 'author_id': fields.many2one('res.users', 'Author'),
967 'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
971 'author_id': lambda x, y, z, c: z,
972 'date': lambda *a: time.strftime('%Y-%m-%d'),
975 stock_production_lot_revision()
977 # ----------------------------------------------------
979 # ----------------------------------------------------
983 # location_dest_id is only used for predicting futur stocks
985 class stock_move(osv.osv):
986 def _getSSCC(self, cr, uid, context={}):
987 cr.execute('select id from stock_tracking where create_uid=%s order by id desc limit 1', (uid,))
989 return (res and res[0]) or False
991 _description = "Stock Move"
993 def name_get(self, cr, uid, ids, context={}):
995 for line in self.browse(cr, uid, ids, context):
996 res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
999 def _check_tracking(self, cr, uid, ids):
1000 for move in self.browse(cr, uid, ids):
1001 if not move.prodlot_id and \
1002 (move.state == 'done' and \
1004 (move.product_id.track_production and move.location_id.usage == 'production') or \
1005 (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
1006 (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
1007 (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') \
1012 def _check_product_lot(self, cr, uid, ids):
1013 for move in self.browse(cr, uid, ids):
1014 if move.prodlot_id and move.state == 'done' and (move.prodlot_id.product_id.id != move.product_id.id):
1019 'name': fields.char('Name', size=64, required=True, select=True),
1020 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1022 'date': fields.datetime('Date Created'),
1023 'date_planned': fields.datetime('Date', required=True, help="Scheduled date for the movement of the products or real date if the move is done."),
1025 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
1027 'product_qty': fields.float('Quantity', required=True),
1028 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1029 'product_uos_qty': fields.float('Quantity (UOS)'),
1030 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1031 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1033 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
1034 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
1035 'address_id': fields.many2one('res.partner.address', 'Dest. Address'),
1037 'prodlot_id': fields.many2one('stock.production.lot', 'Production Lot', help="Production lot is used to put a serial number on the production"),
1038 'tracking_id': fields.many2one('stock.tracking', 'Tracking Lot', select=True, help="Tracking lot is the code that will be put on the logistical unit/pallet"),
1039 # 'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
1041 'auto_validate': fields.boolean('Auto Validate'),
1043 'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
1044 'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
1045 'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
1046 'picking_id': fields.many2one('stock.picking', 'Packing List', select=True),
1048 'note': fields.text('Notes'),
1050 'state': fields.selection([('draft', 'Draft'), ('waiting', 'Waiting'), ('confirmed', 'Confirmed'), ('assigned', 'Available'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', readonly=True, select=True),
1051 'price_unit': fields.float('Unit Price',
1052 digits=(16, int(config['price_accuracy']))),
1056 'You must assign a production lot for this product',
1058 (_check_product_lot,
1059 'You try to assign a lot which is not from the same product',
1062 def _default_location_destination(self, cr, uid, context={}):
1063 if context.get('move_line', []):
1064 if context['move_line'][0]:
1065 if isinstance(context['move_line'][0], (tuple, list)):
1066 return context['move_line'][0][2] and context['move_line'][0][2]['location_dest_id'] or False
1068 move_list = self.pool.get('stock.move').read(cr, uid, context['move_line'][0], ['location_dest_id'])
1069 return move_list and move_list['location_dest_id'][0] or False
1070 if context.get('address_out_id', False):
1071 return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
1074 def _default_location_source(self, cr, uid, context={}):
1075 if context.get('move_line', []):
1077 return context['move_line'][0][2]['location_id']
1080 if context.get('address_in_id', False):
1081 return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
1085 'location_id': _default_location_source,
1086 'location_dest_id': _default_location_destination,
1087 'state': lambda *a: 'draft',
1088 'priority': lambda *a: '1',
1089 'product_qty': lambda *a: 1.0,
1090 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1091 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1094 def _auto_init(self, cursor, context):
1095 res = super(stock_move, self)._auto_init(cursor, context)
1096 cursor.execute('SELECT indexname \
1098 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1099 if not cursor.fetchone():
1100 cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1101 ON stock_move (location_id, location_dest_id, product_id, state)')
1105 def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False, loc_id=False, context=None):
1106 if not prodlot_id or not loc_id:
1108 ctx = context and context.copy() or {}
1109 ctx['location_id'] = loc_id
1110 prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, ctx)
1111 location = self.pool.get('stock.location').browse(cr, uid, loc_id)
1113 if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
1115 'title': _('Bad Lot Assignation !'),
1116 'message': _('You are moving %.2f products but only %.2f available in this lot.') % (product_qty, prodlot.stock_available or 0.0)
1118 return {'warning': warning}
1120 def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1122 'product_uos_qty': 0.00
1125 if (not product_id) or (product_qty <=0.0):
1126 return {'value': result}
1128 product_obj = self.pool.get('product.product')
1129 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1131 if product_uos and product_uom and (product_uom != product_uos):
1132 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1134 result['product_uos_qty'] = product_qty
1136 return {'value': result}
1138 def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty, product_uos, product_uom):
1143 if (not product_id) or (product_uos_qty <=0.0):
1144 return {'value': result}
1146 product_obj = self.pool.get('product.product')
1147 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1149 if product_uos and product_uom and (product_uom != product_uos):
1150 result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1152 result['product_qty'] = product_uos_qty
1154 return {'value': result}
1156 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, address_id=False):
1161 addr_rec = self.pool.get('res.partner.address').browse(cr, uid, address_id)
1163 lang = addr_rec.partner_id and addr_rec.partner_id.lang or False
1164 ctx = {'lang': lang}
1166 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1167 uos_id = product.uos_id and product.uos_id.id or False
1169 'name': product.partner_ref,
1170 'product_uom': product.uom_id.id,
1171 'product_uos': uos_id,
1172 'product_qty': 1.00,
1173 'product_uos_qty' : self.pool.get('stock.move').onchange_quantity(cr, uid, ids, prod_id, 1.00, product.uom_id.id, uos_id)['value']['product_uos_qty']
1177 result['location_id'] = loc_id
1179 result['location_dest_id'] = loc_dest_id
1180 return {'value': result}
1182 def _chain_compute(self, cr, uid, moves, context={}):
1185 dest = self.pool.get('stock.location').chained_location_get(
1189 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1194 if dest[1] == 'transparent':
1195 self.write(cr, uid, [m.id], {
1196 'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
1197 DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
1198 'location_dest_id': dest[0].id})
1200 result.setdefault(m.picking_id, [])
1201 result[m.picking_id].append( (m, dest) )
1204 def action_confirm(self, cr, uid, ids, context={}):
1205 # ids = map(lambda m: m.id, moves)
1206 moves = self.browse(cr, uid, ids)
1207 self.write(cr, uid, ids, {'state': 'confirmed'})
1210 def create_chained_picking(self, cr, uid, moves, context):
1212 picking_obj = self.pool.get('stock.picking')
1213 move_obj = self.pool.get('stock.move')
1214 for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1215 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1216 check_picking_ids = picking_obj.search(cr, uid, [('name','=',picking.name),('origin','=',tools.ustr(picking.origin or '')),('type','=',ptype),('move_type','=',picking.move_type)])
1217 if check_picking_ids:
1218 pickid = check_picking_ids[0]
1221 pickid = picking_obj.create(cr, uid, {
1222 'name': picking.name,
1223 'origin': tools.ustr(picking.origin or ''),
1225 'note': picking.note,
1226 'move_type': picking.move_type,
1227 'auto_picking': todo[0][1][1] == 'auto',
1228 'address_id': picking.address_id.id,
1229 'invoice_state': 'none'
1233 for move, (loc, auto, delay) in todo:
1234 # Is it smart to copy ? May be it's better to recreate ?
1235 new_id = move_obj.copy(cr, uid, move.id, {
1236 'location_id': move.location_dest_id.id,
1237 'location_dest_id': loc.id,
1238 'date_moved': time.strftime('%Y-%m-%d'),
1239 'picking_id': pickid,
1241 'move_history_ids': [],
1242 'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
1243 'move_history_ids2': []}
1245 move_obj.write(cr, uid, [move.id], {
1246 'move_dest_id': new_id,
1247 'move_history_ids': [(4, new_id)]
1249 new_moves.append(self.browse(cr, uid, [new_id])[0])
1251 wf_service = netsvc.LocalService("workflow")
1252 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1254 create_chained_picking(self, cr, uid, new_moves, context)
1255 create_chained_picking(self, cr, uid, moves, context)
1258 def action_assign(self, cr, uid, ids, *args):
1260 for move in self.browse(cr, uid, ids):
1261 if move.state in ('confirmed', 'waiting'):
1262 todo.append(move.id)
1263 res = self.check_assign(cr, uid, todo)
1266 def force_assign(self, cr, uid, ids, context={}):
1267 self.write(cr, uid, ids, {'state': 'assigned'})
1270 def cancel_assign(self, cr, uid, ids, context={}):
1271 self.write(cr, uid, ids, {'state': 'confirmed'})
1275 # Duplicate stock.move
1277 def check_assign(self, cr, uid, ids, context={}):
1281 for move in self.browse(cr, uid, ids):
1282 if move.product_id.type == 'consu':
1283 if move.state in ('confirmed', 'waiting'):
1284 done.append(move.id)
1285 pickings[move.picking_id.id] = 1
1287 if move.state in ('confirmed', 'waiting'):
1288 # Important: we must pass lock=True to _product_reserve() to avoid race conditions and double reservations
1289 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}, lock=True)
1291 #_product_available_test depends on the next status for correct functioning
1292 #the test does not work correctly if the same product occurs multiple times
1293 #in the same order. This is e.g. the case when using the button 'split in two' of
1294 #the stock outgoing form
1295 self.write(cr, uid, move.id, {'state':'assigned'})
1296 done.append(move.id)
1297 pickings[move.picking_id.id] = 1
1299 cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
1303 move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
1304 done.append(move_id)
1307 self.write(cr, uid, done, {'state': 'assigned'})
1310 for pick_id in pickings:
1311 wf_service = netsvc.LocalService("workflow")
1312 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1316 # Cancel move => cancel others move and pickings
1318 def action_cancel(self, cr, uid, ids, context={}):
1322 for move in self.browse(cr, uid, ids):
1323 if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
1325 pickings[move.picking_id.id] = True
1326 if move.move_dest_id and move.move_dest_id.state == 'waiting':
1327 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1328 if context.get('call_unlink',False) and move.move_dest_id.picking_id:
1329 wf_service = netsvc.LocalService("workflow")
1330 wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1331 self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1332 if not context.get('call_unlink',False):
1333 for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
1334 if all(move.state == 'cancel' for move in pick.move_lines):
1335 self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
1337 wf_service = netsvc.LocalService("workflow")
1339 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1340 #self.action_cancel(cr,uid, ids2, context)
1343 def action_done(self, cr, uid, ids, context=None):
1346 for move in self.browse(cr, uid, ids):
1347 if move.move_dest_id.id and (move.state != 'done'):
1348 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
1349 if move.move_dest_id.state in ('waiting', 'confirmed'):
1350 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1351 if move.move_dest_id.picking_id:
1352 wf_service = netsvc.LocalService("workflow")
1353 wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1356 # self.action_done(cr, uid, [move.move_dest_id.id])
1357 if move.move_dest_id.auto_validate:
1358 self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1361 # Accounting Entries
1363 if move.state in ['done','cancel']:
1365 move_ids.append(move.id)
1368 if move.location_id.account_id:
1369 acc_src = move.location_id.account_id.id
1370 if move.location_dest_id.account_id:
1371 acc_dest = move.location_dest_id.account_id.id
1372 if acc_src or acc_dest:
1373 test = [('product.product', move.product_id.id)]
1374 if move.product_id.categ_id:
1375 test.append( ('product.category', move.product_id.categ_id.id) )
1377 acc_src = move.product_id.product_tmpl_id.\
1378 property_stock_account_input.id
1380 acc_src = move.product_id.categ_id.\
1381 property_stock_account_input_categ.id
1383 raise osv.except_osv(_('Error!'),
1384 _('There is no stock input account defined ' \
1385 'for this product: "%s" (id: %d)') % \
1386 (move.product_id.name,
1387 move.product_id.id,))
1389 acc_dest = move.product_id.product_tmpl_id.\
1390 property_stock_account_output.id
1392 acc_dest = move.product_id.categ_id.\
1393 property_stock_account_output_categ.id
1395 raise osv.except_osv(_('Error!'),
1396 _('There is no stock output account defined ' \
1397 'for this product: "%s" (id: %d)') % \
1398 (move.product_id.name,
1399 move.product_id.id,))
1400 if not move.product_id.categ_id.property_stock_journal.id:
1401 raise osv.except_osv(_('Error!'),
1402 _('There is no journal defined '\
1403 'on the product category: "%s" (id: %d)') % \
1404 (move.product_id.categ_id.name,
1405 move.product_id.categ_id.id,))
1406 journal_id = move.product_id.categ_id.property_stock_journal.id
1407 if acc_src != acc_dest:
1408 ref = move.picking_id and move.picking_id.name or False
1409 product_uom_obj = self.pool.get('product.uom')
1410 default_uom = move.product_id.uom_id.id
1411 q = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
1412 if move.product_id.cost_method == 'average' and move.price_unit:
1413 amount = q * move.price_unit
1415 amount = q * move.product_id.standard_price
1417 date = time.strftime('%Y-%m-%d')
1420 partner_id = move.picking_id.address_id and (move.picking_id.address_id.partner_id and move.picking_id.address_id.partner_id.id or False) or False
1424 'quantity': move.product_qty,
1425 'product_id': move.product_id and move.product_id.id or False,
1427 'account_id': acc_src,
1430 'partner_id': partner_id}),
1433 'product_id': move.product_id and move.product_id.id or False,
1434 'quantity': move.product_qty,
1436 'account_id': acc_dest,
1439 'partner_id': partner_id})
1441 self.pool.get('account.move').create(cr, uid, {
1443 'journal_id': journal_id,
1448 self.write(cr, uid, move_ids, {'state': 'done', 'date_planned': time.strftime('%Y-%m-%d %H:%M:%S')})
1449 wf_service = netsvc.LocalService("workflow")
1451 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1454 def unlink(self, cr, uid, ids, context=None):
1457 ctx = context.copy()
1458 for move in self.browse(cr, uid, ids, context=ctx):
1459 if move.state != 'draft' and not ctx.get('call_unlink',False):
1460 raise osv.except_osv(_('UserError'),
1461 _('You can only delete draft moves.'))
1462 return super(stock_move, self).unlink(
1463 cr, uid, ids, context=ctx)
1468 class stock_inventory(osv.osv):
1469 _name = "stock.inventory"
1470 _description = "Inventory"
1472 'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
1473 'date': fields.datetime('Date create', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1474 'date_done': fields.datetime('Date done'),
1475 'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft': [('readonly', False)]}),
1476 'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1477 'state': fields.selection( (('draft', 'Draft'), ('done', 'Done')), 'Status', readonly=True),
1480 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1481 'state': lambda *a: 'draft',
1485 # Update to support tracking
1487 def action_done(self, cr, uid, ids, context=None):
1488 for inv in self.browse(cr, uid, ids):
1491 for line in inv.inventory_line_id:
1492 pid = line.product_id.id
1493 price = line.product_id.standard_price or 0.0
1494 amount = self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1495 change = line.product_qty - amount
1497 location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1499 'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
1500 'product_id': line.product_id.id,
1501 'product_uom': line.product_uom.id,
1503 'date_planned': inv.date,
1508 'product_qty': change,
1509 'location_id': location_id,
1510 'location_dest_id': line.location_id.id,
1514 'product_qty': -change,
1515 'location_id': line.location_id.id,
1516 'location_dest_id': location_id,
1518 move_ids.append(self.pool.get('stock.move').create(cr, uid, value))
1520 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1522 self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6, 0, move_ids)]})
1525 def action_cancel(self, cr, uid, ids, context={}):
1526 for inv in self.browse(cr, uid, ids):
1527 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1528 self.write(cr, uid, [inv.id], {'state': 'draft'})
1534 class stock_inventory_line(osv.osv):
1535 _name = "stock.inventory.line"
1536 _description = "Inventory line"
1538 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
1539 'location_id': fields.many2one('stock.location', 'Location', required=True),
1540 'product_id': fields.many2one('product.product', 'Product', required=True),
1541 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1542 'product_qty': fields.float('Quantity')
1545 def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
1549 prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
1550 uom = prod.uom_id.id
1551 amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
1552 result = {'product_qty': amount, 'product_uom': uom}
1553 return {'value': result}
1555 stock_inventory_line()
1558 #----------------------------------------------------------
1560 #----------------------------------------------------------
1561 class stock_warehouse(osv.osv):
1562 _name = "stock.warehouse"
1563 _description = "Warehouse"
1565 'name': fields.char('Name', size=60, required=True),
1566 # 'partner_id': fields.many2one('res.partner', 'Owner'),
1567 'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
1568 'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True, domain=[('usage','<>','view')]),
1569 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage','<>','view')]),
1570 'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True, domain=[('usage','<>','view')]),
1577 # get confirm or assign stock move lines of partner and put in current picking.
1578 class stock_picking_move_wizard(osv.osv_memory):
1579 _name = 'stock.picking.move.wizard'
1581 def _get_picking(self, cr, uid, ctx):
1582 if ctx.get('action_id', False):
1583 return ctx['action_id']
1586 def _get_picking_address(self, cr, uid, ctx):
1587 picking_obj = self.pool.get('stock.picking')
1588 if ctx.get('action_id', False):
1589 picking = picking_obj.browse(cr, uid, [ctx['action_id']])[0]
1590 return picking.address_id and picking.address_id.id or False
1594 'name': fields.char('Name', size=64, invisible=True),
1595 #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
1596 'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines', required=True),
1597 'address_id': fields.many2one('res.partner.address', 'Dest. Address', invisible=True),
1598 'picking_id': fields.many2one('stock.picking', 'Packing list', select=True, invisible=True),
1601 'picking_id': _get_picking,
1602 'address_id': _get_picking_address,
1605 def action_move(self, cr, uid, ids, context=None):
1606 move_obj = self.pool.get('stock.move')
1607 picking_obj = self.pool.get('stock.picking')
1608 for act in self.read(cr, uid, ids):
1609 move_lines = move_obj.browse(cr, uid, act['move_ids'])
1610 for line in move_lines:
1612 picking_obj.write(cr, uid, [line.picking_id.id], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1613 picking_obj.write(cr, uid, [act['picking_id']], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1615 old_picking = picking_obj.read(cr, uid, [line.picking_id.id])[0]
1616 if not len(old_picking['move_lines']):
1617 picking_obj.write(cr, uid, [old_picking['id']], {'state': 'done'})
1619 raise osv.except_osv(_('UserError'),
1620 _('You can not create new moves.'))
1621 return {'type': 'ir.actions.act_window_close'}
1623 stock_picking_move_wizard()
1626 class report_stock_lines_date(osv.osv):
1627 _name = "report.stock.lines.date"
1628 _description = "Dates of Inventories"
1631 'product_id': fields.many2one('product.product', 'Product Id', readonly=True),
1632 'create_date': fields.datetime('Latest Date of Inventory'),
1637 create or replace view report_stock_lines_date as (
1641 max(l.create_date) as create_date
1645 stock_inventory_line l on (p.id=l.product_id)
1646 where l.create_date is not null
1650 report_stock_lines_date()