1 ##############################################################################
3 # OpenERP, Open Source Management Solution
4 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as
8 # published by the Free Software Foundation, either version 3 of the
9 # License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 ##############################################################################
21 from datetime import datetime
22 from dateutil.relativedelta import relativedelta
24 from osv import fields, osv
25 from tools import config
26 from tools.translate import _
32 import decimal_precision as dp
35 #----------------------------------------------------------
37 #----------------------------------------------------------
38 class stock_incoterms(osv.osv):
39 _name = "stock.incoterms"
40 _description = "Incoterms"
42 'name': fields.char('Name', size=64, required=True,help="Incoterms are series of sales terms.They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices."),
43 'code': fields.char('Code', size=3, required=True,help="Code for Incoterms"),
44 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the incoterms without removing it."),
47 'active': lambda *a: True,
53 #----------------------------------------------------------
55 #----------------------------------------------------------
56 class stock_location(osv.osv):
57 _name = "stock.location"
58 _description = "Location"
59 _parent_name = "location_id"
62 _order = 'parent_left'
64 def name_get(self, cr, uid, ids, context={}):
67 reads = self.read(cr, uid, ids, ['name','location_id'], context)
71 if context.get('full',False):
72 if record['location_id']:
73 name = record['location_id'][1]+' / '+name
74 res.append((record['id'], name))
76 res.append((record['id'], name))
79 def _complete_name(self, cr, uid, ids, name, args, context):
80 """ Forms complete name of location from parent location to child location.
81 @return: Dictionary of values
83 def _get_one_full_name(location, level=4):
84 if location.location_id:
85 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
88 return parent_path + location.name
90 for m in self.browse(cr, uid, ids, context=context):
91 res[m.id] = _get_one_full_name(m)
94 def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
95 """ Finds real and virtual quantity for product available at particular location.
96 @return: Dictionary of values
100 res[id] = {}.fromkeys(field_names, 0.0)
101 if ('product_id' not in context) or not ids:
103 #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
105 context['location'] = [loc]
106 prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
107 if 'stock_real' in field_names:
108 res[loc]['stock_real'] = prod.qty_available
109 if 'stock_virtual' in field_names:
110 res[loc]['stock_virtual'] = prod.virtual_available
113 def product_detail(self, cr, uid, id, field, context={}):
114 """ Finds detail of product like price type, currency and then calculates its price.
115 @param field: Field name
116 @return: Calculated price
121 field_to_read = 'virtual_available'
122 if field == 'stock_real_value':
123 field_to_read = 'qty_available'
124 cr.execute('select distinct product_id from stock_move where (location_id=%s) or (location_dest_id=%s)', (id, id))
125 result = cr.dictfetchall()
127 # Choose the right filed standard_price to read
128 # Take the user company
129 price_type_id = self.pool.get('res.users').browse(cr,uid,uid).company_id.property_valuation_price_type.id
130 pricetype = self.pool.get('product.price.type').browse(cr, uid, price_type_id)
132 c = (context or {}).copy()
134 product = self.pool.get('product.product').read(cr, uid, r['product_id'], [field_to_read], context=c)
135 # Compute the amount_unit in right currency
137 context['currency_id'] = self.pool.get('res.users').browse(cr,uid,uid).company_id.currency_id.id
138 amount_unit = self.pool.get('product.product').browse(cr,uid,r['product_id']).price_get(pricetype.field, context)[r['product_id']]
140 final_value += (product[field_to_read] * amount_unit)
143 def _product_value(self, cr, uid, ids, field_names, arg, context={}):
144 """ Calculates real and virtual stock value of a product.
145 @param field_names: Name of field
146 @return: Dictionary of values
150 result[id] = {}.fromkeys(field_names, 0.0)
151 for field_name in field_names:
153 ret_dict = self.product_detail(cr, uid, loc, field=field_name)
154 result[loc][field_name] = ret_dict
158 'name': fields.char('Location Name', size=64, required=True, translate=True),
159 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the stock location without removing it."),
160 'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Type', required=True),
161 'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True),
163 'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
165 'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
166 'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
168 #'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type', '!=', 'view')]),
169 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
170 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
172 'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
173 'chained_location_type': fields.selection([('none', 'None'), ('customer', 'Customer'), ('fixed', 'Fixed Location')],
174 'Chained Location Type', required=True),
175 'chained_auto_packing': fields.selection(
176 [('auto', 'Automatic Move'), ('manual', 'Manual Operation'), ('transparent', 'Automatic No Step Added')],
179 help="This is used only if you select a chained location type.\n" \
180 "The 'Automatic Move' value will create a stock move after the current one that will be "\
181 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
182 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
184 'chained_delay': fields.integer('Chained lead time (days)'),
185 'address_id': fields.many2one('res.partner.address', 'Location Address'),
186 'icon': fields.selection(tools.icons, 'Icon', size=64),
188 'comment': fields.text('Additional Information'),
189 'posx': fields.integer('Corridor (X)'),
190 'posy': fields.integer('Shelves (Y)'),
191 'posz': fields.integer('Height (Z)'),
193 'parent_left': fields.integer('Left Parent', select=1),
194 'parent_right': fields.integer('Right Parent', select=1),
195 'stock_real_value': fields.function(_product_value, method=True, type='float', string='Real Stock Value', multi="stock"),
196 'stock_virtual_value': fields.function(_product_value, method=True, type='float', string='Virtual Stock Value', multi="stock"),
197 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared for every companies'),
198 'scrap_location': fields.boolean('Scrap Location', help='Check this box if the current location is a place for destroyed items'),
201 'active': lambda *a: 1,
202 'usage': lambda *a: 'internal',
203 'allocation_method': lambda *a: 'fifo',
204 'chained_location_type': lambda *a: 'none',
205 'chained_auto_packing': lambda *a: 'manual',
206 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
207 'posx': lambda *a: 0,
208 'posy': lambda *a: 0,
209 'posz': lambda *a: 0,
210 'icon': lambda *a: False,
211 'scrap_location': lambda *a: False,
214 def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
215 """ Finds chained location
216 @param location: Location id
217 @param partner: Partner id
218 @param product: Product id
219 @return: List of values
222 if location.chained_location_type == 'customer':
224 result = partner.property_stock_customer
225 elif location.chained_location_type == 'fixed':
226 result = location.chained_location_id
228 return result, location.chained_auto_packing, location.chained_delay
231 def picking_type_get(self, cr, uid, from_location, to_location, context={}):
232 """ Gets type of picking.
233 @param from_location: Source location
234 @param to_location: Destination location
235 @return: Location type
238 if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer', 'supplier')):
240 elif (from_location.usage in ('supplier', 'customer')) and (to_location.usage=='internal'):
244 def _product_get_all_report(self, cr, uid, ids, product_ids=False,
246 return self._product_get_report(cr, uid, ids, product_ids, context,
249 def _product_get_report(self, cr, uid, ids, product_ids=False,
250 context=None, recursive=False):
251 """ Finds the product quantity and price for particular location.
252 @param product_ids: Ids of product
253 @param recursive: True or False
254 @return: Dictionary of values
258 product_obj = self.pool.get('product.product')
259 # Take the user company and pricetype
260 price_type_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.property_valuation_price_type.id
261 pricetype = self.pool.get('product.price.type').browse(cr, uid, price_type_id)
262 context['currency_id'] = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
265 product_ids = product_obj.search(cr, uid, [])
267 products = product_obj.browse(cr, uid, product_ids, context=context)
270 for product in products:
271 products_by_uom.setdefault(product.uom_id.id, [])
272 products_by_uom[product.uom_id.id].append(product)
273 products_by_id.setdefault(product.id, [])
274 products_by_id[product.id] = product
277 result['product'] = []
281 for uom_id in products_by_uom.keys():
282 fnc = self._product_get
284 fnc = self._product_all_get
287 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
289 for product_id in qty.keys():
290 if not qty[product_id]:
292 product = products_by_id[product_id]
293 quantity_total += qty[product_id]
295 # Compute based on pricetype
296 # Choose the right filed standard_price to read
297 amount_unit = product.price_get(pricetype.field, context)[product.id]
298 price = qty[product_id] * amount_unit
299 # price = qty[product_id] * product.standard_price
302 result['product'].append({
303 'price': amount_unit,
304 'prod_name': product.name,
305 'code': product.default_code, # used by lot_overview_all report!
306 'variants': product.variants or '',
307 'uom': product.uom_id.name,
308 'prod_qty': qty[product_id],
309 'price_value': price,
311 result['total'] = quantity_total
312 result['total_price'] = total_price
315 def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={},
316 states=['done'], what=('in', 'out')):
318 @param product_ids: Ids of product
319 @param states: List of states
320 @param what: Tuple of
323 product_obj = self.pool.get('product.product')
329 return product_obj.get_product_available(cr, uid, product_ids, context=context)
331 def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
337 ids = id and [id] or []
338 return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
340 def _product_all_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
341 # build the list of ids of children of the location given by id
342 ids = id and [id] or []
343 location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
344 return self._product_get_multi_location(cr, uid, location_ids, product_ids, context, states)
346 def _product_virtual_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
347 return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
351 # Improve this function
354 # [ (tracking_id, product_qty, location_id) ]
356 def _product_reserve(self, cr, uid, ids, product_id, product_qty, context={}):
358 @param product_id: Id of product
359 @param product_qty: Quantity of product
360 @return: List of Values or False
364 for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
365 cr.execute("select product_uom,sum(product_qty) as product_qty from stock_move where location_dest_id=%s and location_id<>%s and product_id=%s and state='done' group by product_uom", (id, id, product_id))
366 results = cr.dictfetchall()
367 cr.execute("select product_uom,-sum(product_qty) as product_qty from stock_move where location_id=%s and location_dest_id<>%s and product_id=%s and state in ('done', 'assigned') group by product_uom", (id, id, product_id))
368 results += cr.dictfetchall()
373 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
382 if amount > min(total, product_qty):
383 amount = min(product_qty, total)
384 result.append((amount, id))
385 product_qty -= amount
387 if product_qty <= 0.0:
396 class stock_tracking(osv.osv):
397 _name = "stock.tracking"
398 _description = "Stock Tracking Lots"
400 def get_create_tracking_lot(self, cr, uid, ids, tracking_lot):
402 @param tracking_lot: Name of tracking lot
405 tracking_lot_list = self.search(cr, uid, [('name', '=', tracking_lot)],
407 if tracking_lot_list:
408 tracking_lot = tracking_lot_list[0]
409 tracking_obj = self.browse(cr, uid, tracking_lot)
411 tracking_lot_vals = {
414 tracking_lot = self.create(cr, uid, tracking_lot_vals)
417 salt = '31' * 8 + '3'
419 for sscc_part, salt_part in zip(sscc, salt):
420 sum += int(sscc_part) * int(salt_part)
421 return (10 - (sum % 10)) % 10
422 checksum = staticmethod(checksum)
424 def make_sscc(self, cr, uid, context={}):
425 sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
426 return sequence + str(self.checksum(sequence))
429 'name': fields.char('Tracking ID', size=64, required=True),
430 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the tracking lots without removing it."),
431 'serial': fields.char('Reference', size=64),
432 'move_ids': fields.one2many('stock.move', 'tracking_id', 'Moves Tracked'),
433 'date': fields.datetime('Created Date', required=True),
437 'active': lambda *a: 1,
439 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
442 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
447 ids = self.search(cr, user, [('serial', '=', name)]+ args, limit=limit, context=context)
448 ids += self.search(cr, user, [('name', operator, name)]+ args, limit=limit, context=context)
449 return self.name_get(cr, user, ids, context)
451 def name_get(self, cr, uid, ids, context={}):
454 res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name', 'serial'], context)]
457 def unlink(self, cr, uid, ids, context=None):
458 raise osv.except_osv(_('Error'), _('You can not remove a lot line !'))
463 #----------------------------------------------------------
465 #----------------------------------------------------------
466 class stock_picking(osv.osv):
467 _name = "stock.picking"
468 _description = "Picking List"
470 def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
471 """ Calculates planned date if it is greater than 'value'.
472 @param name: Name of field
473 @param value: Value of field
474 @param arg: User defined argument
475 @return: True or False
479 if isinstance(ids, (int, long)):
481 for pick in self.browse(cr, uid, ids, context):
482 sql_str = """update stock_move set
485 picking_id=%d """ % (value, pick.id)
488 sql_str += " and (date_planned='" + pick.max_date + "' or date_planned>'" + value + "')"
492 def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
493 """ Calculates planned date if it is less than 'value'.
494 @param name: Name of field
495 @param value: Value of field
496 @param arg: User defined argument
497 @return: True or False
501 if isinstance(ids, (int, long)):
503 for pick in self.browse(cr, uid, ids, context):
504 sql_str = """update stock_move set
507 picking_id=%s """ % (value, pick.id)
509 sql_str += " and (date_planned='" + pick.min_date + "' or date_planned<'" + value + "')"
513 def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
514 """ Finds minimum and maximum dates for picking.
515 @return: Dictionary of values
519 res[id] = {'min_date': False, 'max_date': False}
531 picking_id""",(ids,))
532 for pick, dt1, dt2 in cr.fetchall():
533 res[pick]['min_date'] = dt1
534 res[pick]['max_date'] = dt2
537 def create(self, cr, user, vals, context=None):
538 if ('name' not in vals) or (vals.get('name')=='/'):
539 seq_obj_name = 'stock.picking.' + vals['type']
540 vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name)
542 'out':_('Packing List'),
544 'internal': _('Internal picking'),
545 'delivery': _('Delivery order')
547 new_id = super(stock_picking, self).create(cr, user, vals, context)
548 if not vals.get('auto_picking', False):
549 message = type_list.get(vals.get('type', False), _('Picking')) + " '" + (vals['name'] or "n/a") + _(" with origin")+" '" + (vals['origin'] or "n/a") + "' "+ _("is created.")
550 self.log(cr, user, new_id, message)
554 'name': fields.char('Reference', size=64, select=True),
555 'origin': fields.char('Origin', size=64, help="Reference of the document that produced this picking."),
556 'backorder_id': fields.many2one('stock.picking', 'Back Order', help="If the picking is splitted then the picking id in available state of move for this picking is stored in Backorder."),
557 'type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal'), ('delivery', 'Delivery')], 'Shipping Type', required=True, select=True, help="Shipping type specify, goods coming in or going out."),
558 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the picking without removing it."),
559 'note': fields.text('Notes'),
561 'location_id': fields.many2one('stock.location', 'Location', help="Keep empty if you produce at the location where the finished products are needed." \
562 "Set a location if you produce at a fixed location. This can be a partner location " \
563 "if you subcontract the manufacturing operations."),
564 'location_dest_id': fields.many2one('stock.location', 'Dest. Location',help="Location where the system will stock the finished products."),
565 'move_type': fields.selection([('direct', 'Direct Delivery'), ('one', 'All at once')], 'Delivery Method', required=True, help="It specifies goods to be delivered all at once or by direct delivery"),
566 'state': fields.selection([
569 ('confirmed', 'Confirmed'),
570 ('assigned', 'Available'),
572 ('cancel', 'Cancelled'),
573 ], 'State', readonly=True, select=True,
574 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed picking. \
575 \n* The \'Confirmed\' state is used for stock movement to do with unavailable products. \
576 \n* The \'Available\' state is set automatically when the products are ready to be moved.\
577 \n* The \'Waiting\' state is used in MTO moves when a movement is waiting for another one.'),
578 'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
579 method=True, store=True, type='datetime', string='Expected Date', select=1, help="Expected date for Picking. Default it takes current date"),
580 'date': fields.datetime('Order Date', help="Date of Order"),
581 'date_done': fields.datetime('Date Done', help="Date of Completion"),
582 'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
583 method=True, store=True, type='datetime', string='Max. Expected Date', select=2),
584 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
585 'delivery_line':fields.one2many('stock.delivery', 'picking_id', 'Delivery lines', readonly=True),
586 'auto_picking': fields.boolean('Auto-Picking'),
587 'address_id': fields.many2one('res.partner.address', 'Partner', help="Address of partner"),
588 'invoice_state': fields.selection([
589 ("invoiced", "Invoiced"),
590 ("2binvoiced", "To Be Invoiced"),
591 ("none", "Not from Picking")], "Invoice Status",
592 select=True, required=True, readonly=True, states={'draft': [('readonly', False)]}),
593 'company_id': fields.many2one('res.company', 'Company', required=True,select=1),
596 'name': lambda self, cr, uid, context: '/',
597 'active': lambda *a: 1,
598 'state': lambda *a: 'draft',
599 'move_type': lambda *a: 'direct',
600 'type': lambda *a: 'in',
601 'invoice_state': lambda *a: 'none',
602 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
603 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock_picking', context=c)
606 def copy(self, cr, uid, id, default=None, context={}):
609 default = default.copy()
610 picking_obj = self.browse(cr, uid, [id], context)[0]
611 if ('name' not in default) or (picking_obj.name=='/'):
612 seq_obj_name = 'stock.picking.' + picking_obj.type
613 default['name'] = self.pool.get('ir.sequence').get(cr, uid, seq_obj_name)
615 return super(stock_picking, self).copy(cr, uid, id, default, context)
617 def onchange_partner_in(self, cr, uid, context, partner_id=None):
620 def action_explode(self, cr, uid, moves, context={}):
623 def action_confirm(self, cr, uid, ids, context={}):
624 """ Confirms picking.
627 self.write(cr, uid, ids, {'state': 'confirmed'})
629 for picking in self.browse(cr, uid, ids, context=context):
630 for r in picking.move_lines:
631 if r.state == 'draft':
633 todo = self.action_explode(cr, uid, todo, context)
635 self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
638 def test_auto_picking(self, cr, uid, ids):
639 # TODO: Check locations to see if in the same location ?
642 # def button_confirm(self, cr, uid, ids, *args):
644 # wf_service = netsvc.LocalService("workflow")
645 # wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
646 # self.force_assign(cr, uid, ids, *args)
649 def action_assign(self, cr, uid, ids, *args):
650 """ Changes state of picking to available if all moves are confirmed.
653 for pick in self.browse(cr, uid, ids):
654 move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
656 raise osv.except_osv(_('Warning !'),_('Not Available. Moves are not confirmed.'))
657 self.pool.get('stock.move').action_assign(cr, uid, move_ids)
660 def force_assign(self, cr, uid, ids, *args):
661 """ Changes state of picking to available if moves are confirmed or waiting.
664 wf_service = netsvc.LocalService("workflow")
665 for pick in self.browse(cr, uid, ids):
666 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','waiting']]
667 # move_ids = [x.id for x in pick.move_lines]
668 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
669 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
672 def draft_force_assign(self, cr, uid, ids, *args):
673 """ Confirms picking directly from draft state.
676 wf_service = netsvc.LocalService("workflow")
677 for pick in self.browse(cr, uid, ids):
678 wf_service.trg_validate(uid, 'stock.picking', pick.id,
679 'button_confirm', cr)
680 #move_ids = [x.id for x in pick.move_lines]
681 #self.pool.get('stock.move').force_assign(cr, uid, move_ids)
682 #wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
685 def draft_validate(self, cr, uid, ids, *args):
686 """ Validates picking directly from draft state.
689 wf_service = netsvc.LocalService("workflow")
690 self.draft_force_assign(cr, uid, ids)
691 for pick in self.browse(cr, uid, ids):
692 move_ids = [x.id for x in pick.move_lines]
693 self.pool.get('stock.move').force_assign(cr, uid, move_ids)
694 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
696 self.action_move(cr, uid, [pick.id])
697 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
700 def cancel_assign(self, cr, uid, ids, *args):
701 """ Cancels picking and moves.
704 wf_service = netsvc.LocalService("workflow")
705 for pick in self.browse(cr, uid, ids):
706 move_ids = [x.id for x in pick.move_lines]
707 self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
708 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
711 def action_assign_wkf(self, cr, uid, ids, context=None):
712 """ Changes picking state to assigned.
715 for pick in self.browse(cr, uid, ids, context=context):
717 'out':'Packing List',
719 'internal': 'Internal picking',
720 'delivery': 'Delivery order'
722 message = type_list.get(pick.type, _('Document')) + " '" + (pick.name or 'n/a') + "' "+ _("is ready to be processed.")
723 self.log(cr, uid, id, message)
724 self.write(cr, uid, ids, {'state': 'assigned'})
727 def test_finnished(self, cr, uid, ids):
728 """ Tests whether the move is in done or cancel state or not.
729 @return: True or False
731 move_ids = self.pool.get('stock.move').search(cr, uid, [('picking_id', 'in', ids)])
732 for move in self.pool.get('stock.move').browse(cr, uid, move_ids):
733 if move.state not in ('done', 'cancel'):
734 if move.product_qty != 0.0:
737 move.write(cr, uid, [move.id], {'state': 'done'})
740 def test_assigned(self, cr, uid, ids):
741 """ Tests whether the move is in assigned state or not.
742 @return: True or False
745 for pick in self.browse(cr, uid, ids):
747 for move in pick.move_lines:
748 if (move.state in ('confirmed', 'draft')) and (mt == 'one'):
750 if (mt == 'direct') and (move.state == 'assigned') and (move.product_qty):
752 ok = ok and (move.state in ('cancel', 'done', 'assigned'))
755 def action_cancel(self, cr, uid, ids, context={}):
756 """ Changes picking state to cancel.
759 for pick in self.browse(cr, uid, ids):
760 ids2 = [move.id for move in pick.move_lines]
761 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
762 self.write(cr, uid, ids, {'state': 'cancel', 'invoice_state': 'none'})
766 # TODO: change and create a move if not parents
768 def action_done(self, cr, uid, ids, context=None):
769 """ Changes picking state to done.
772 self.write(cr, uid, ids, {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
775 def action_move(self, cr, uid, ids, context={}):
776 """ Changes move state to assigned.
779 for pick in self.browse(cr, uid, ids):
781 for move in pick.move_lines:
782 if move.state == 'assigned':
786 self.pool.get('stock.move').action_done(cr, uid, todo,
790 def get_currency_id(self, cr, uid, picking):
793 def _get_payment_term(self, cr, uid, picking):
794 """ Gets payment term from partner.
795 @return: Payment term
797 partner_obj = self.pool.get('res.partner')
798 partner = picking.address_id.partner_id
799 return partner.property_payment_term and partner.property_payment_term.id or False
801 def _get_address_invoice(self, cr, uid, picking):
802 """ Gets invoice address of a partner
803 @return {'contact': address, 'invoice': address} for invoice
805 partner_obj = self.pool.get('res.partner')
806 partner = picking.address_id.partner_id
808 return partner_obj.address_get(cr, uid, [partner.id],
809 ['contact', 'invoice'])
811 def _get_comment_invoice(self, cr, uid, picking):
813 @return: comment string for invoice
815 return picking.note or ''
817 def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):
818 """ Gets price unit for invoice
819 @param move_line: Stock move lines
820 @param type: Type of invoice
821 @return: The price unit for the move line
826 if type in ('in_invoice', 'in_refund'):
827 # Take the user company and pricetype
828 price_type_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.property_valuation_price_type.id
829 pricetype = self.pool.get('product.price.type').browse(cr, uid, price_type_id)
830 context['currency_id'] = move_line.company_id.currency_id.id
832 amount_unit = move_line.product_id.price_get(pricetype.field, context)[move_line.product_id.id]
835 return move_line.product_id.list_price
837 def _get_discount_invoice(self, cr, uid, move_line):
838 '''Return the discount for the move line'''
841 def _get_taxes_invoice(self, cr, uid, move_line, type):
842 """ Gets taxes on invoice
843 @param move_line: Stock move lines
844 @param type: Type of invoice
845 @return: Taxes Ids for the move line
847 if type in ('in_invoice', 'in_refund'):
848 taxes = move_line.product_id.supplier_taxes_id
850 taxes = move_line.product_id.taxes_id
852 if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
853 return self.pool.get('account.fiscal.position').map_tax(
856 move_line.picking_id.address_id.partner_id.property_account_position,
860 return map(lambda x: x.id, taxes)
862 def _get_account_analytic_invoice(self, cr, uid, picking, move_line):
865 def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id):
866 '''Call after the creation of the invoice line'''
869 def _invoice_hook(self, cr, uid, picking, invoice_id):
870 '''Call after the creation of the invoice'''
873 def action_invoice_create(self, cr, uid, ids, journal_id=False,
874 group=False, type='out_invoice', context=None):
875 """ Creates invoice based on the invoice state selected for picking.
876 @param journal_id: Id of journal
877 @param group: Whether to create a group invoice or not
878 @param type: Type invoice to be created
879 @return: Ids of created invoices for the pickings
884 invoice_obj = self.pool.get('account.invoice')
885 invoice_line_obj = self.pool.get('account.invoice.line')
889 for picking in self.browse(cr, uid, ids, context=context):
890 if picking.invoice_state != '2binvoiced':
892 payment_term_id = False
893 partner = picking.address_id and picking.address_id.partner_id
895 raise osv.except_osv(_('Error, no partner !'),
896 _('Please put a partner on the picking list if you want to generate invoice.'))
898 if type in ('out_invoice', 'out_refund'):
899 account_id = partner.property_account_receivable.id
900 payment_term_id = self._get_payment_term(cr, uid, picking)
902 account_id = partner.property_account_payable.id
904 address_contact_id, address_invoice_id = \
905 self._get_address_invoice(cr, uid, picking).values()
907 comment = self._get_comment_invoice(cr, uid, picking)
908 if group and partner.id in invoices_group:
909 invoice_id = invoices_group[partner.id]
910 invoice = invoice_obj.browse(cr, uid, invoice_id)
912 'name': (invoice.name or '') + ', ' + (picking.name or ''),
913 'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
914 'comment': (comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
915 'date_invoice':context.get('date_inv',False),
916 'user_id':picking.sale_id.user_id and picking.sale_id.user_id.id or False
918 invoice_obj.write(cr, uid, [invoice_id], invoice_vals, context=context)
921 'name': picking.name,
922 'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
924 'account_id': account_id,
925 'partner_id': partner.id,
926 'address_invoice_id': address_invoice_id,
927 'address_contact_id': address_contact_id,
929 'payment_term': payment_term_id,
930 'fiscal_position': partner.property_account_position.id,
931 'date_invoice': context.get('date_inv',False),
932 'company_id': picking.company_id.id,
933 'user_id':picking.sale_id.user_id and picking.sale_id.user_id.id or False
935 cur_id = self.get_currency_id(cr, uid, picking)
937 invoice_vals['currency_id'] = cur_id
939 invoice_vals['journal_id'] = journal_id
940 invoice_id = invoice_obj.create(cr, uid, invoice_vals,
942 invoices_group[partner.id] = invoice_id
943 res[picking.id] = invoice_id
944 for move_line in picking.move_lines:
945 origin = move_line.picking_id.name or ''
946 if move_line.picking_id.origin:
947 origin += ':' + move_line.picking_id.origin
949 name = (picking.name or '') + '-' + move_line.name
951 name = move_line.name
953 if type in ('out_invoice', 'out_refund'):
954 account_id = move_line.product_id.product_tmpl_id.\
955 property_account_income.id
957 account_id = move_line.product_id.categ_id.\
958 property_account_income_categ.id
960 account_id = move_line.product_id.product_tmpl_id.\
961 property_account_expense.id
963 account_id = move_line.product_id.categ_id.\
964 property_account_expense_categ.id
966 price_unit = self._get_price_unit_invoice(cr, uid,
968 discount = self._get_discount_invoice(cr, uid, move_line)
969 tax_ids = self._get_taxes_invoice(cr, uid, move_line, type)
970 account_analytic_id = self._get_account_analytic_invoice(cr, uid, picking, move_line)
972 #set UoS if it's a sale and the picking doesn't have one
973 uos_id = move_line.product_uos and move_line.product_uos.id or False
974 if not uos_id and type in ('out_invoice', 'out_refund'):
975 uos_id = move_line.product_uom.id
977 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
979 if ('sale_line_id' in move_line._columns.keys()) and move_line.sale_line_id:
980 notes = move_line.sale_line_id.notes
981 elif ('purchase_line_id' in move_line._columns.keys()) and move_line.purchase_line_id:
982 notes = move_line.purchase_line_id.notes
984 invoice_line_id = invoice_line_obj.create(cr, uid, {
987 'invoice_id': invoice_id,
989 'product_id': move_line.product_id.id,
990 'account_id': account_id,
991 'price_unit': price_unit,
992 'discount': discount,
993 'quantity': move_line.product_uos_qty or move_line.product_qty,
994 'invoice_line_tax_id': [(6, 0, tax_ids)],
995 'account_analytic_id': account_analytic_id,
998 self._invoice_line_hook(cr, uid, move_line, invoice_line_id)
1000 invoice_obj.button_compute(cr, uid, [invoice_id], context=context,
1001 set_total=(type in ('in_invoice', 'in_refund')))
1002 self.write(cr, uid, [picking.id], {
1003 'invoice_state': 'invoiced',
1005 self._invoice_hook(cr, uid, picking, invoice_id)
1006 self.write(cr, uid, res.keys(), {
1007 'invoice_state': 'invoiced',
1011 def test_cancel(self, cr, uid, ids, context={}):
1012 """ Test whether the move lines are canceled or not.
1013 @return: True or False
1015 for pick in self.browse(cr, uid, ids, context=context):
1016 if not pick.move_lines:
1018 for move in pick.move_lines:
1019 if move.state not in ('cancel',):
1023 def unlink(self, cr, uid, ids, context=None):
1024 move_obj = self.pool.get('stock.move')
1028 for pick in self.browse(cr, uid, ids, context=context):
1029 if pick.state in ['done','cancel']:
1030 raise osv.except_osv(_('Error'), _('You cannot remove the picking which is in %s state !')%(pick.state,))
1031 elif pick.state in ['confirmed','assigned', 'draft']:
1032 ids2 = [move.id for move in pick.move_lines]
1033 ctx = context.copy()
1034 ctx.update({'call_unlink':True})
1035 if pick.state != 'draft':
1036 #Cancelling the move in order to affect Virtual stock of product
1037 move_obj.action_cancel(cr, uid, ids2, ctx)
1039 move_obj.unlink(cr, uid, ids2, ctx)
1041 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
1043 def do_partial(self, cr, uid, ids, partial_datas, context={}):
1044 """ Makes partial picking and moves done.
1045 @param partial_datas : Dictionary containing details of partial picking
1046 like partner_id, address_id, delivery_date,
1047 delivery moves with product_id, product_qty, uom
1048 @return: Dictionary of values
1052 move_obj = self.pool.get('stock.move')
1053 delivery_obj = self.pool.get('stock.delivery')
1054 product_obj = self.pool.get('product.product')
1055 currency_obj = self.pool.get('res.currency')
1056 users_obj = self.pool.get('res.users')
1057 uom_obj = self.pool.get('product.uom')
1058 price_type_obj = self.pool.get('product.price.type')
1059 sequence_obj = self.pool.get('ir.sequence')
1060 wf_service = netsvc.LocalService("workflow")
1061 partner_id = partial_datas.get('partner_id', False)
1062 address_id = partial_datas.get('address_id', False)
1063 delivery_date = partial_datas.get('delivery_date', False)
1064 for pick in self.browse(cr, uid, ids, context=context):
1068 complete, too_many, too_few = [], [], []
1069 move_product_qty = {}
1070 for move in pick.move_lines:
1071 if move.state in ('done', 'cancel'):
1073 partial_data = partial_datas.get('move%s'%(move.id), False)
1074 assert partial_data, _('Do not Found Partial data of Stock Move Line :%s' %(move.id))
1075 product_qty = partial_data.get('product_qty',0.0)
1076 move_product_qty[move.id] = product_qty
1077 product_uom = partial_data.get('product_uom',False)
1078 product_price = partial_data.get('product_price',0.0)
1079 product_currency = partial_data.get('product_currency',False)
1080 if move.product_qty == product_qty:
1081 complete.append(move)
1082 elif move.product_qty > product_qty:
1083 too_few.append(move)
1085 too_many.append(move)
1087 # Average price computation
1088 if (pick.type == 'in') and (move.product_id.cost_method == 'average'):
1089 product = product_obj.browse(cr, uid, move.product_id.id)
1090 user = users_obj.browse(cr, uid, uid)
1091 context['currency_id'] = move.company_id.currency_id.id
1092 qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
1094 if user.company_id.property_valuation_price_type:
1095 pricetype = price_type_obj.browse(cr, uid, user.company_id.property_valuation_price_type.id)
1096 if pricetype and qty > 0:
1097 new_price = currency_obj.compute(cr, uid, product_currency,
1098 user.company_id.currency_id.id, product_price)
1099 new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
1101 if product.qty_available <= 0:
1102 new_std_price = new_price
1104 # Get the standard price
1105 amount_unit = product.price_get(pricetype.field, context)[product.id]
1106 new_std_price = ((amount_unit * product.qty_available)\
1107 + (new_price * qty))/(product.qty_available + qty)
1109 # Write the field according to price type field
1110 product_obj.write(cr, uid, [product.id],
1111 {pricetype.field: new_std_price})
1112 move_obj.write(cr, uid, [move.id], {'price_unit': new_price})
1115 for move in too_few:
1116 product_qty = move_product_qty[move.id]
1119 new_picking = self.copy(cr, uid, pick.id,
1121 'name': sequence_obj.get(cr, uid, 'stock.picking.%s'%(pick.type)),
1125 if product_qty != 0:
1127 new_obj = move_obj.copy(cr, uid, move.id,
1129 'product_qty' : product_qty,
1130 'product_uos_qty': product_qty, #TODO: put correct uos_qty
1131 'picking_id' : new_picking,
1132 'state': 'assigned',
1133 'move_dest_id': False,
1134 'price_unit': move.price_unit,
1137 move_obj.write(cr, uid, [move.id],
1139 'product_qty' : move.product_qty - product_qty,
1140 'product_uos_qty':move.product_qty - product_qty, #TODO: put correct uos_qty
1145 move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking})
1146 for move in too_many:
1147 product_qty = move_product_qty[move.id]
1148 move_obj.write(cr, uid, [move.id],
1150 'product_qty' : product_qty,
1151 'product_uos_qty': product_qty, #TODO: put correct uos_qty
1152 'picking_id': new_picking,
1155 for move in too_many:
1156 product_qty = move_product_qty[move.id]
1157 move_obj.write(cr, uid, [move.id],
1159 'product_qty': product_qty,
1160 'product_uos_qty': product_qty #TODO: put correct uos_qty
1163 # At first we confirm the new picking (if necessary)
1165 wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
1166 # Then we finish the good picking
1168 self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
1169 self.action_move(cr, uid, [new_picking])
1170 wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr)
1171 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
1172 delivered_pack_id = new_picking
1174 self.action_move(cr, uid, [pick.id])
1175 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
1176 delivered_pack_id = pick.id
1178 delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
1179 delivery_id = delivery_obj.create(cr, uid, {
1180 'name': delivered_pack.name or move.name,
1181 'partner_id': partner_id,
1182 'address_id': address_id,
1183 'date': delivery_date,
1184 'picking_id' : pick.id,
1185 'move_delivered' : [(6,0, map(lambda x:x.id, delivered_pack.move_lines))]
1187 res[pick.id] = {'delivered_picking': delivered_pack.id or False}
1193 class stock_production_lot(osv.osv):
1194 def name_get(self, cr, uid, ids, context={}):
1197 reads = self.read(cr, uid, ids, ['name', 'prefix', 'ref'], context)
1199 for record in reads:
1200 name = record['name']
1201 prefix = record['prefix']
1203 name = prefix + '/' + name
1205 name = '%s [%s]' % (name, record['ref'])
1206 res.append((record['id'], name))
1209 _name = 'stock.production.lot'
1210 _description = 'Production lot'
1212 def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
1213 """ Gets stock of products for locations
1214 @return: Dictionary of values
1216 if 'location_id' not in context:
1217 locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
1219 locations = context['location_id'] and [context['location_id']] or []
1221 if isinstance(ids, (int, long)):
1224 res = {}.fromkeys(ids, 0.0)
1226 cr.execute('''select
1230 stock_report_prodlots
1232 location_id =ANY(%s) and prodlot_id =ANY(%s) group by prodlot_id''',(locations,ids,))
1233 res.update(dict(cr.fetchall()))
1236 def _stock_search(self, cr, uid, obj, name, args, context):
1237 """ Searches Ids of products
1238 @return: Ids of locations
1240 locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')])
1241 cr.execute('''select
1245 stock_report_prodlots
1247 location_id =ANY(%s) group by prodlot_id
1248 having sum(name) '''+ str(args[0][1]) + str(args[0][2]),(locations,))
1250 ids = [('id', 'in', map(lambda x: x[0], res))]
1254 'name': fields.char('Serial', size=64, required=True),
1255 'ref': fields.char('Internal Reference', size=256),
1256 'prefix': fields.char('Prefix', size=64),
1257 'product_id': fields.many2one('product.product', 'Product', required=True),
1258 'date': fields.datetime('Created Date', required=True),
1259 'stock_available': fields.function(_get_stock, fnct_search=_stock_search, method=True, type="float", string="Available", select="2"),
1260 'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'),
1261 'company_id': fields.many2one('res.company','Company',select=1),
1265 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1266 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1267 'product_id': lambda x, y, z, c: c.get('product_id', False),
1269 _sql_constraints = [
1270 ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
1273 stock_production_lot()
1275 class stock_production_lot_revision(osv.osv):
1276 _name = 'stock.production.lot.revision'
1277 _description = 'Production lot revisions'
1280 'name': fields.char('Revision Name', size=64, required=True),
1281 'description': fields.text('Description'),
1282 'date': fields.date('Revision Date'),
1283 'indice': fields.char('Revision', size=16),
1284 'author_id': fields.many2one('res.users', 'Author'),
1285 'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
1286 'company_id': fields.related('lot_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
1290 'author_id': lambda x, y, z, c: z,
1291 'date': lambda *a: time.strftime('%Y-%m-%d'),
1294 stock_production_lot_revision()
1296 class stock_delivery(osv.osv):
1298 """ Traceability of partial deliveries """
1300 _name = "stock.delivery"
1301 _description = "Delivery"
1304 'name': fields.char('Name', size=60, required=True),
1305 'date': fields.datetime('Date', required=True),
1306 'partner_id': fields.many2one('res.partner', 'Partner', required=True),
1307 'address_id': fields.many2one('res.partner.address', 'Address', required=True),
1308 'move_delivered':fields.one2many('stock.move', 'delivered_id', 'Move Delivered'),
1309 'picking_id': fields.many2one('stock.picking', 'Picking list'),
1313 # ----------------------------------------------------
1315 # ----------------------------------------------------
1319 # location_dest_id is only used for predicting futur stocks
1321 class stock_move(osv.osv):
1322 def _getSSCC(self, cr, uid, context={}):
1323 cr.execute('select id from stock_tracking where create_uid=%s order by id desc limit 1', (uid,))
1325 return (res and res[0]) or False
1326 _name = "stock.move"
1327 _description = "Stock Move"
1330 def name_get(self, cr, uid, ids, context={}):
1332 for line in self.browse(cr, uid, ids, context):
1333 res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
1336 def _check_tracking(self, cr, uid, ids):
1337 """ Checks if production lot is assigned to stock move or not.
1338 @return: True or False
1340 for move in self.browse(cr, uid, ids):
1341 if not move.prodlot_id and \
1342 (move.state == 'done' and \
1344 (move.product_id.track_production and move.location_id.usage=='production') or \
1345 (move.product_id.track_production and move.location_dest_id.usage=='production') or \
1346 (move.product_id.track_incoming and move.location_id.usage in ('supplier','internal')) or \
1347 (move.product_id.track_outgoing and move.location_dest_id.usage in ('customer','internal')) \
1352 def _check_product_lot(self, cr, uid, ids):
1353 """ Checks whether move is done or not and production lot is assigned to that move.
1354 @return: True or False
1356 for move in self.browse(cr, uid, ids):
1357 if move.prodlot_id and move.state == 'done' and (move.prodlot_id.product_id.id != move.product_id.id):
1362 'name': fields.char('Name', size=64, required=True, select=True),
1363 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1365 'date': fields.datetime('Created Date'),
1366 'date_planned': fields.datetime('Date Planned', required=True, help="Scheduled date for the movement of the products or real date if the move is done."),
1367 'date_expected': fields.datetime('Date Expected', readonly=True,required=True, help="Scheduled date for the movement of the products"),
1368 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
1370 'product_qty': fields.float('Quantity', required=True),
1371 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True),
1372 'product_uos_qty': fields.float('Quantity (UOS)'),
1373 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1374 'product_packaging': fields.many2one('product.packaging', 'Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1376 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True, help="Sets a location if you produce at a fixed location. This can be a partner location if you subcontract the manufacturing operations."),
1377 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True, help="Location where the system will stock the finished products."),
1378 'address_id': fields.many2one('res.partner.address', 'Dest. Address', help="Address where goods are to be delivered"),
1380 'prodlot_id': fields.many2one('stock.production.lot', 'Production Lot', help="Production lot is used to put a serial number on the production"),
1381 '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"),
1382 # 'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
1384 'auto_validate': fields.boolean('Auto Validate'),
1386 'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
1387 'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
1388 'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
1389 'picking_id': fields.many2one('stock.picking', 'Picking List', select=True),
1391 'note': fields.text('Notes'),
1393 'state': fields.selection([('draft', 'Draft'), ('waiting', 'Waiting'), ('confirmed', 'Confirmed'), ('assigned', 'Available'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', readonly=True, select=True,
1394 help='When the stock move is created it is in the \'Draft\' state.\n After that it is set to \'Confirmed\' state.\n If stock is available state is set to \'Avaiable\'.\n When the picking it done the state is \'Done\'.\
1395 \nThe state is \'Waiting\' if the move is waiting for another one.'),
1396 'price_unit': fields.float('Unit Price',
1397 digits_compute= dp.get_precision('Account')),
1398 'company_id': fields.many2one('res.company', 'Company', required=True,select=1),
1399 'partner_id': fields.related('picking_id','address_id','partner_id',type='many2one', relation="res.partner", string="Partner"),
1400 'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Orders"),
1401 'origin': fields.related('picking_id','origin',type='char', size=64, relation="stock.picking", string="Origin"),
1402 'move_stock_return_history': fields.many2many('stock.move', 'stock_move_return_history', 'move_id', 'return_move_id', 'Move Return History',readonly=True),
1403 'delivered_id': fields.many2one('stock.delivery', 'Product delivered'),
1404 'scraped': fields.related('location_dest_id','scrap_location',type='boolean',relation='stock.location',string='Scraped'),
1408 'You must assign a production lot for this product',
1410 (_check_product_lot,
1411 'You try to assign a lot which is not from the same product',
1414 def _default_location_destination(self, cr, uid, context={}):
1415 """ Gets default address of partner for destination location
1416 @return: Address id or False
1418 if context.get('move_line', []):
1419 if context['move_line'][0]:
1420 if isinstance(context['move_line'][0], (tuple, list)):
1421 return context['move_line'][0][2] and context['move_line'][0][2]['location_dest_id'] or False
1423 move_list = self.pool.get('stock.move').read(cr, uid, context['move_line'][0], ['location_dest_id'])
1424 return move_list and move_list['location_dest_id'][0] or False
1425 if context.get('address_out_id', False):
1426 return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
1429 def _default_location_source(self, cr, uid, context={}):
1430 """ Gets default address of partner for source location
1431 @return: Address id or False
1433 if context.get('move_line', []):
1435 return context['move_line'][0][2]['location_id']
1438 if context.get('address_in_id', False):
1439 return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
1443 'location_id': _default_location_source,
1444 'location_dest_id': _default_location_destination,
1445 'state': lambda *a: 'draft',
1446 'priority': lambda *a: '1',
1447 'product_qty': lambda *a: 1.0,
1448 'scraped' : lambda *a: False,
1449 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1450 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1451 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1452 'date_expected': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1455 def copy(self, cr, uid, id, default=None, context={}):
1458 default = default.copy()
1459 default['move_stock_return_history'] = []
1460 return super(stock_move, self).copy(cr, uid, id, default, context)
1462 def create(self, cr, user, vals, context=None):
1463 if vals.get('move_stock_return_history',False):
1464 vals['move_stock_return_history'] = []
1465 # Check that the stock.move is in draft state at creation to force
1466 # passing through button_confirm
1467 if vals.get('state','draft') not in ('draft','done','waiting'):
1468 logger = netsvc.Logger()
1469 logger.notifyChannel("code", netsvc.LOG_WARNING, "All new stock.move must be in state draft at the creation !")
1470 return super(stock_move, self).create(cr, user, vals, context)
1472 def _auto_init(self, cursor, context):
1473 res = super(stock_move, self)._auto_init(cursor, context)
1474 cursor.execute('SELECT indexname \
1476 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1477 if not cursor.fetchone():
1478 cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1479 ON stock_move (location_id, location_dest_id, product_id, state)')
1483 def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False,
1484 loc_id=False, product_id=False, context=None):
1485 """ On change of production lot gives a warning message.
1486 @param prodlot_id: Changed production lot id
1487 @param product_qty: Quantity of product
1488 @param loc_id: Location id
1489 @param product_id: Product id
1490 @return: Warning message
1492 if not prodlot_id or not loc_id:
1494 ctx = context and context.copy() or {}
1495 ctx['location_id'] = loc_id
1496 prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, ctx)
1497 location = self.pool.get('stock.location').browse(cr, uid, loc_id)
1499 if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
1501 'title': 'Bad Lot Assignation !',
1502 'message': 'You are moving %.2f products but only %.2f available in this lot.' % (product_qty, prodlot.stock_available or 0.0)
1504 return {'warning': warning}
1506 def onchange_quantity(self, cr, uid, ids, product_id, product_qty,
1507 product_uom, product_uos):
1508 """ On change of product quantity finds UoM and UoS quantities
1509 @param product_id: Product id
1510 @param product_qty: Changed Quantity of product
1511 @param product_uom: Unit of measure of product
1512 @param product_uos: Unit of sale of product
1513 @return: Dictionary of values
1516 'product_uos_qty': 0.00
1519 if (not product_id) or (product_qty <=0.0):
1520 return {'value': result}
1522 product_obj = self.pool.get('product.product')
1523 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1525 if product_uos and product_uom and (product_uom != product_uos):
1526 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1528 result['product_uos_qty'] = product_qty
1530 return {'value': result}
1532 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False,
1533 loc_dest_id=False, address_id=False):
1534 """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1535 @param prod_id: Changed Product id
1536 @param loc_id: Source location id
1537 @param loc_id: Destination location id
1538 @param address_id: Address id of partner
1539 @return: Dictionary of values
1545 addr_rec = self.pool.get('res.partner.address').browse(cr, uid, address_id)
1547 lang = addr_rec.partner_id and addr_rec.partner_id.lang or False
1548 ctx = {'lang': lang}
1550 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1551 uos_id = product.uos_id and product.uos_id.id or False
1553 'product_uom': product.uom_id.id,
1554 'product_uos': uos_id,
1555 'product_qty': 1.00,
1556 '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']
1559 result['name'] = product.partner_ref
1561 result['location_id'] = loc_id
1563 result['location_dest_id'] = loc_dest_id
1564 return {'value': result}
1566 def _chain_compute(self, cr, uid, moves, context={}):
1567 """ Finds whether the location has chained location type or not.
1568 @param moves: Stock moves
1569 @return: Dictionary containing destination location with chained location type.
1573 dest = self.pool.get('stock.location').chained_location_get(
1577 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1582 if dest[1] == 'transparent':
1583 self.write(cr, uid, [m.id], {
1584 'date_planned': (datetime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
1585 relativedelta(days=dest[2] or 0)).strftime('%Y-%m-%d'),
1586 'location_dest_id': dest[0].id})
1588 result.setdefault(m.picking_id, [])
1589 result[m.picking_id].append( (m, dest) )
1592 def action_confirm(self, cr, uid, ids, context={}):
1593 """ Confirms stock move.
1594 @return: List of ids.
1596 # ids = map(lambda m: m.id, moves)
1597 moves = self.browse(cr, uid, ids)
1598 self.write(cr, uid, ids, {'state': 'confirmed'})
1601 def create_chained_picking(self, cr, uid, moves, context):
1603 for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1604 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1606 if ptype == 'delivery':
1607 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.delivery')
1608 pickid = self.pool.get('stock.picking').create(cr, uid, {
1609 'name': pick_name or picking.name,
1610 'origin': str(picking.origin or ''),
1612 'note': picking.note,
1613 'move_type': picking.move_type,
1614 'auto_picking': todo[0][1][1] == 'auto',
1615 'address_id': picking.address_id.id,
1616 'invoice_state': 'none'
1618 for move, (loc, auto, delay) in todo:
1619 # Is it smart to copy ? May be it's better to recreate ?
1620 new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
1621 'location_id': move.location_dest_id.id,
1622 'location_dest_id': loc.id,
1623 'date_moved': time.strftime('%Y-%m-%d'),
1624 'picking_id': pickid,
1626 'move_history_ids': [],
1627 'date_planned': (datetime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + relativedelta(days=delay or 0)).strftime('%Y-%m-%d'),
1628 'move_history_ids2': []}
1630 self.pool.get('stock.move').write(cr, uid, [move.id], {
1631 'move_dest_id': new_id,
1632 'move_history_ids': [(4, new_id)]
1634 new_moves.append(self.browse(cr, uid, [new_id])[0])
1635 wf_service = netsvc.LocalService("workflow")
1636 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1638 create_chained_picking(self, cr, uid, new_moves, context)
1639 create_chained_picking(self, cr, uid, moves, context)
1642 def action_assign(self, cr, uid, ids, *args):
1643 """ Changes state to confirmed or waiting.
1644 @return: List of values
1647 for move in self.browse(cr, uid, ids):
1648 if move.state in ('confirmed', 'waiting'):
1649 todo.append(move.id)
1650 res = self.check_assign(cr, uid, todo)
1653 def force_assign(self, cr, uid, ids, context={}):
1654 """ Changes the state to assigned.
1657 self.write(cr, uid, ids, {'state': 'assigned'})
1660 def cancel_assign(self, cr, uid, ids, context={}):
1661 """ Changes the state to confirmed.
1664 self.write(cr, uid, ids, {'state': 'confirmed'})
1668 # Duplicate stock.move
1670 def check_assign(self, cr, uid, ids, context={}):
1671 """ Checks the product type and accordingly writes the state.
1672 @return: No. of moves done
1677 for move in self.browse(cr, uid, ids):
1678 if move.product_id.type == 'consu':
1679 if move.state in ('confirmed', 'waiting'):
1680 done.append(move.id)
1681 pickings[move.picking_id.id] = 1
1683 if move.state in ('confirmed', 'waiting'):
1684 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})
1686 #_product_available_test depends on the next status for correct functioning
1687 #the test does not work correctly if the same product occurs multiple times
1688 #in the same order. This is e.g. the case when using the button 'split in two' of
1689 #the stock outgoing form
1690 self.write(cr, uid, move.id, {'state':'assigned'})
1691 done.append(move.id)
1692 pickings[move.picking_id.id] = 1
1694 cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
1698 move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
1699 done.append(move_id)
1700 #cr.execute('insert into stock_move_history_ids values (%s,%s)', (move.id,move_id))
1703 self.write(cr, uid, done, {'state': 'assigned'})
1706 for pick_id in pickings:
1707 wf_service = netsvc.LocalService("workflow")
1708 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1712 # Cancel move => cancel others move and pickings
1714 def action_cancel(self, cr, uid, ids, context={}):
1715 """ Cancels the moves and if all moves are cancelled it cancels the picking.
1721 for move in self.browse(cr, uid, ids):
1722 if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
1724 pickings[move.picking_id.id] = True
1725 if move.move_dest_id and move.move_dest_id.state == 'waiting':
1726 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1727 if context.get('call_unlink',False) and move.move_dest_id.picking_id:
1728 wf_service = netsvc.LocalService("workflow")
1729 wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1730 self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1731 if not context.get('call_unlink',False):
1732 for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
1733 if all(move.state == 'cancel' for move in pick.move_lines):
1734 self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
1736 wf_service = netsvc.LocalService("workflow")
1738 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1739 #self.action_cancel(cr,uid, ids2, context)
1742 def _get_accounting_values(self, cr, uid, move, context=None):
1743 product_obj=self.pool.get('product.product')
1744 product_uom_obj = self.pool.get('product.uom')
1745 price_type_obj = self.pool.get('product.price.type')
1746 accounts = product_obj.get_product_accounts(cr,uid,move.product_id.id,context)
1747 acc_src = accounts['stock_account_input']
1748 acc_dest = accounts['stock_account_output']
1749 acc_variation = accounts['property_stock_variation']
1750 journal_id = accounts['stock_journal']
1753 raise osv.except_osv(_('Error!'), _('There is no stock input account defined ' \
1754 'for this product: "%s" (id: %d)') % \
1755 (move.product_id.name, move.product_id.id,))
1757 raise osv.except_osv(_('Error!'), _('There is no stock output account defined ' \
1758 'for this product: "%s" (id: %d)') % \
1759 (move.product_id.name, move.product_id.id,))
1761 raise osv.except_osv(_('Error!'), _('There is no journal defined '\
1762 'on the product category: "%s" (id: %d)') % \
1763 (move.product_id.categ_id.name, move.product_id.categ_id.id,))
1764 if not acc_variation:
1765 raise osv.except_osv(_('Error!'), _('There is no variation account defined '\
1766 'on the product category: "%s" (id: %d)') % \
1767 (move.product_id.categ_id.name, move.product_id.categ_id.id,))
1768 if acc_src != acc_dest:
1769 default_uom = move.product_id.uom_id.id
1770 q = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
1771 if move.product_id.cost_method == 'average' and move.price_unit:
1772 amount = q * move.price_unit
1773 # Base computation on valuation price type
1775 company_id = move.company_id.id
1776 context['currency_id'] = move.company_id.currency_id.id
1777 pricetype = price_type_obj.browse(cr,uid,move.company_id.property_valuation_price_type.id)
1778 amount_unit = move.product_id.price_get(pricetype.field, context)[move.product_id.id]
1779 amount = amount_unit * q or 1.0
1780 # amount = q * move.product_id.standard_price
1781 return journal_id, acc_src, acc_dest, acc_variation, amount
1784 def action_done(self, cr, uid, ids, context={}):
1785 """ Makes the move done and if all moves are done, it will finish the picking.
1790 product_uom_obj = self.pool.get('product.uom')
1791 price_type_obj = self.pool.get('product.price.type')
1792 product_obj=self.pool.get('product.product')
1793 move_obj = self.pool.get('account.move')
1794 for move in self.browse(cr, uid, ids):
1795 if move.picking_id: picking_ids.append(move.picking_id.id)
1796 if move.move_dest_id.id and (move.state != 'done'):
1797 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
1798 if move.move_dest_id.state in ('waiting', 'confirmed'):
1799 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1800 if move.move_dest_id.picking_id:
1801 wf_service = netsvc.LocalService("workflow")
1802 wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1805 # self.action_done(cr, uid, [move.move_dest_id.id])
1806 if move.move_dest_id.auto_validate:
1807 self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1810 # Accounting Entries
1814 if move.product_id.valuation == 'real_time':
1816 if ((move.location_id.usage == 'internal' and move.location_dest_id.usage == 'customer') or (move.location_id.usage == 'internal' and move.location_dest_id.usage == 'transit')):
1817 if move.location_id.company_id:
1818 context.update({'force_company': move.location_id.company_id.id})
1819 journal_id, acc_src, acc_dest, acc_variation, amount = self._get_accounting_values(cr, uid, move, context)
1820 lines = [(journal_id, self.create_account_move(cr, uid, move, acc_dest, acc_variation, amount, context))]
1822 elif ((move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'internal') or (move.location_id.usage == 'transit' and move.location_dest_id.usage == 'internal')):
1823 if move.location_dest_id.company_id:
1824 context.update({'force_company': move.location_dest_id.company_id.id})
1825 journal_id, acc_src, acc_dest, acc_variation, amount = self._get_accounting_values(cr, uid, move, context)
1826 lines = [(journal_id, self.create_account_move(cr, uid, move, acc_variation, acc_src, amount, context))]
1827 elif (move.location_id.usage == 'internal' and move.location_dest_id.usage == 'internal' and move.location_id.company_id != move.location_dest_id.company_id):
1828 if move.location_id.company_id:
1829 context.update({'force_company': move.location_id.company_id.id})
1830 journal_id, acc_src, acc_dest, acc_variation, amount = self._get_accounting_values(cr, uid, move, context)
1831 line1 = [(journal_id, self.create_account_move(cr, uid, move, acc_dest, acc_variation, amount, context))]
1832 if move.location_dest_id.company_id:
1833 context.update({'force_company': move.location_dest_id.company_id.id})
1834 journal_id, acc_src, acc_dest, acc_variation, amount = self._get_accounting_values(cr, uid, move, context)
1835 line2 = [(journal_id, self.create_account_move(cr, uid, move, acc_variation, acc_src, amount, context))]
1836 lines = line1 + line2
1837 for j_id, line in lines:
1838 move_obj.create(cr, uid, {
1841 'type':'cont_voucher',
1843 'ref': move.picking_id and move.picking_id.name,
1845 tracking_lot = False
1847 tracking_lot = context.get('tracking_lot', False)
1849 rec_id = context and context.get('active_id', False)
1850 tracking = self.pool.get('stock.tracking')
1851 tracking_lot = tracking.get_create_tracking_lot(cr, uid,[rec_id], tracking_lot)
1853 self.write(cr, uid, ids, {'state': 'done', 'date_planned': time.strftime('%Y-%m-%d %H:%M:%S'), 'tracking_id': tracking_lot or False})
1854 picking_obj = self.pool.get('stock.picking')
1855 for pick in picking_obj.browse(cr, uid, picking_ids):
1856 if all(move.state == 'done' for move in pick.move_lines):
1857 picking_obj.action_done(cr, uid, [pick.id])
1858 for (id,name) in picking_obj.name_get(cr, uid, [pick.id]):
1859 message = _('Picking ') + " '" + name + "' "+ _("is processed")
1860 self.log(cr, uid, id, message)
1861 wf_service = netsvc.LocalService("workflow")
1863 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1867 def create_account_move(self, cr, uid, move,account_id,account_variation,amount, context=None):
1869 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
1872 'quantity': move.product_qty,
1873 'product_id': move.product_id and move.product_id.id or False,
1875 'account_id': account_id,
1876 'ref': move.picking_id and move.picking_id.name or False,
1877 'date': time.strftime('%Y-%m-%d') ,
1878 'partner_id': partner_id,
1882 'product_id': move.product_id and move.product_id.id or False,
1883 'quantity': move.product_qty,
1885 'account_id': account_variation,
1886 'ref': move.picking_id and move.picking_id.name or False,
1887 'date': time.strftime('%Y-%m-%d') ,
1888 'partner_id': partner_id,
1891 def unlink(self, cr, uid, ids, context=None):
1894 for move in self.browse(cr, uid, ids, context=context):
1895 if move.state != 'draft':
1896 raise osv.except_osv(_('UserError'),
1897 _('You can only delete draft moves.'))
1898 return super(stock_move, self).unlink(
1899 cr, uid, ids, context=context)
1901 def _create_lot(self, cr, uid, ids, product_id, prefix=False):
1902 """ Creates production lot
1903 @return: Production lot id
1905 prodlot_obj = self.pool.get('stock.production.lot')
1906 ir_sequence_obj = self.pool.get('ir.sequence')
1907 sequence = ir_sequence_obj.get(cr, uid, 'stock.lot.serial')
1909 raise osv.except_osv(_('Error!'), _('No production sequence defined'))
1910 prodlot_id = prodlot_obj.create(cr, uid, {'name': sequence, 'prefix': prefix}, {'product_id': product_id})
1911 prodlot = prodlot_obj.browse(cr, uid, prodlot_id)
1912 ref = ','.join(map(lambda x:str(x),ids))
1914 ref = '%s, %s' % (prodlot.ref, ref)
1915 prodlot_obj.write(cr, uid, [prodlot_id], {'ref': ref})
1919 def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
1920 """ Move the scrap/damaged product into scrap location
1921 @param cr: the database cursor
1922 @param uid: the user id
1923 @param ids: ids of stock move object to be scraped
1924 @param quantity : specify scrap qty
1925 @param location_id : specify scrap location
1926 @param context: context arguments
1927 @return: Scraped lines
1930 raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
1932 for move in self.browse(cr, uid, ids, context=context):
1933 move_qty = move.product_qty
1934 uos_qty = quantity / move_qty * move.product_uos_qty
1936 'product_qty': quantity,
1937 'product_uos_qty': uos_qty,
1938 'state': move.state,
1940 'location_dest_id': location_id
1942 new_move = self.copy(cr, uid, move.id, default_val)
1943 #self.write(cr, uid, [new_move], {'move_history_ids':[(4,move.id)]}) #TODO : to track scrap moves
1945 self.action_done(cr, uid, res)
1948 def action_split(self, cr, uid, ids, quantity, split_by_qty=1, prefix=False, with_lot=True, context=None):
1949 """ Split Stock Move lines into production lot which specified split by quantity.
1950 @param cr: the database cursor
1951 @param uid: the user id
1952 @param ids: ids of stock move object to be splited
1953 @param split_by_qty : specify split by qty
1954 @param prefix : specify prefix of production lot
1955 @param with_lot : if true, prodcution lot will assign for split line otherwise not.
1956 @param context: context arguments
1957 @return: Splited move lines
1961 raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
1965 for move in self.browse(cr, uid, ids):
1966 if split_by_qty <= 0 or quantity == 0:
1969 uos_qty = split_by_qty / move.product_qty * move.product_uos_qty
1971 quantity_rest = quantity % split_by_qty
1972 uos_qty_rest = split_by_qty / move.product_qty * move.product_uos_qty
1975 'product_qty': split_by_qty,
1976 'product_uos_qty': uos_qty,
1978 for idx in range(int(quantity//split_by_qty)):
1979 if not idx and move.product_qty<=quantity:
1980 current_move = move.id
1982 current_move = self.copy(cr, uid, move.id, {'state': move.state})
1983 res.append(current_move)
1985 update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
1987 self.write(cr, uid, [current_move], update_val)
1990 if quantity_rest > 0:
1991 idx = int(quantity//split_by_qty)
1992 update_val['product_qty'] = quantity_rest
1993 update_val['product_uos_qty'] = uos_qty_rest
1994 if not idx and move.product_qty<=quantity:
1995 current_move = move.id
1997 current_move = self.copy(cr, uid, move.id, {'state': move.state})
1999 res.append(current_move)
2003 update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
2005 self.write(cr, uid, [current_move], update_val)
2008 def action_consume(self, cr, uid, ids, quantity, location_id=False, context=None):
2009 """ Consumed product with specific quatity from specific source location
2010 @param cr: the database cursor
2011 @param uid: the user id
2012 @param ids: ids of stock move object to be consumed
2013 @param quantity : specify consume quantity
2014 @param location_id : specify source location
2015 @param context: context arguments
2016 @return: Consumed lines
2022 raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
2025 for move in self.browse(cr, uid, ids, context=context):
2026 move_qty = move.product_qty
2027 quantity_rest = move.product_qty
2029 quantity_rest -= quantity
2030 uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
2031 if quantity_rest <= 0:
2034 quantity = move.product_qty
2036 uos_qty = quantity / move_qty * move.product_uos_qty
2038 if quantity_rest > 0:
2040 'product_qty': quantity,
2041 'product_uos_qty': uos_qty,
2042 'state': move.state,
2043 'location_id': location_id
2045 if move.product_id.track_production and location_id:
2046 # IF product has checked track for production lot, move lines will be split by 1
2047 res += self.action_split(cr, uid, [move.id], quantity, split_by_qty=1, context=context)
2049 current_move = self.copy(cr, uid, move.id, default_val)
2050 res += [current_move]
2053 update_val['product_qty'] = quantity_rest
2054 update_val['product_uos_qty'] = uos_qty_rest
2055 self.write(cr, uid, [move.id], update_val)
2058 quantity_rest = quantity
2059 uos_qty_rest = uos_qty
2061 if move.product_id.track_production and location_id:
2062 res += self.split_lines(cr, uid, [move.id], quantity_rest, split_by_qty=1, context=context)
2066 'product_qty' : quantity_rest,
2067 'product_uos_qty' : uos_qty_rest,
2068 'location_id': location_id
2071 self.write(cr, uid, [move.id], update_val)
2073 self.action_done(cr, uid, res)
2076 def do_partial(self, cr, uid, ids, partial_datas, context={}):
2077 """ Makes partial pickings and moves done.
2078 @param partial_datas: Dictionary containing details of partial picking
2079 like partner_id, address_id, delivery_date, delivery
2080 moves with product_id, product_qty, uom
2083 picking_obj = self.pool.get('stock.picking')
2084 delivery_obj = self.pool.get('stock.delivery')
2085 product_obj = self.pool.get('product.product')
2086 currency_obj = self.pool.get('res.currency')
2087 users_obj = self.pool.get('res.users')
2088 uom_obj = self.pool.get('product.uom')
2089 price_type_obj = self.pool.get('product.price.type')
2090 sequence_obj = self.pool.get('ir.sequence')
2091 wf_service = netsvc.LocalService("workflow")
2092 partner_id = partial_datas.get('partner_id', False)
2093 address_id = partial_datas.get('address_id', False)
2094 delivery_date = partial_datas.get('delivery_date', False)
2095 tracking_lot = context.get('tracking_lot', False)
2099 complete, too_many, too_few = [], [], []
2100 move_product_qty = {}
2101 for move in self.browse(cr, uid, ids, context=context):
2102 if move.state in ('done', 'cancel'):
2104 partial_data = partial_datas.get('move%s'%(move.id), False)
2105 assert partial_data, _('Do not Found Partial data of Stock Move Line :%s' %(move.id))
2106 product_qty = partial_data.get('product_qty',0.0)
2107 move_product_qty[move.id] = product_qty
2108 product_uom = partial_data.get('product_uom',False)
2109 product_price = partial_data.get('product_price',0.0)
2110 product_currency = partial_data.get('product_currency',False)
2111 if move.product_qty == product_qty:
2112 self.write(cr, uid, move.id,
2114 'tracking_id': tracking_lot
2116 complete.append(move)
2117 elif move.product_qty > product_qty:
2118 too_few.append(move)
2120 too_many.append(move)
2122 # Average price computation
2123 if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
2124 product = product_obj.browse(cr, uid, move.product_id.id)
2125 user = users_obj.browse(cr, uid, uid)
2126 context['currency_id'] = move.company_id.currency_id.id
2127 qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
2129 if user.company_id.property_valuation_price_type:
2130 pricetype = price_type_obj.browse(cr, uid, user.company_id.property_valuation_price_type.id)
2131 if pricetype and qty > 0:
2132 new_price = currency_obj.compute(cr, uid, product_currency,
2133 user.company_id.currency_id.id, product_price)
2134 new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
2136 if product.qty_available <= 0:
2137 new_std_price = new_price
2139 # Get the standard price
2140 amount_unit = product.price_get(pricetype.field, context)[product.id]
2141 new_std_price = ((amount_unit * product.qty_available)\
2142 + (new_price * qty))/(product.qty_available + qty)
2144 # Write the field according to price type field
2145 product_obj.write(cr, uid, [product.id],
2146 {pricetype.field: new_std_price})
2147 self.write(cr, uid, [move.id], {'price_unit': new_price})
2149 for move in too_few:
2150 product_qty = move_product_qty[move.id]
2151 if product_qty != 0:
2152 new_move = self.copy(cr, uid, move.id,
2154 'product_qty' : product_qty,
2155 'product_uos_qty': product_qty,
2156 'picking_id' : move.picking_id.id,
2157 'state': 'assigned',
2158 'move_dest_id': False,
2159 'price_unit': move.price_unit,
2160 'tracking_id': tracking_lot,
2162 complete.append(self.browse(cr, uid, new_move))
2163 self.write(cr, uid, move.id,
2165 'product_qty' : move.product_qty - product_qty,
2166 'product_uos_qty':move.product_qty - product_qty,
2170 for move in too_many:
2171 self.write(cr, uid, move.id,
2173 'product_qty': move.product_qty,
2174 'product_uos_qty': move.product_qty,
2175 'tracking_id': tracking_lot
2177 complete.append(move)
2179 for move in complete:
2180 self.action_done(cr, uid, [move.id], context)
2182 # TOCHECK : Done picking if all moves are done
2184 SELECT move.id FROM stock_picking pick
2185 RIGHT JOIN stock_move move ON move.picking_id = pick.id AND move.state = %s
2186 WHERE pick.id = %s""",
2187 ('done', move.picking_id.id))
2189 if len(res) == len(move.picking_id.move_lines):
2190 picking_obj.action_move(cr, uid, [move.picking_id.id])
2191 wf_service.trg_validate(uid, 'stock.picking', move.picking_id.id, 'button_done', cr)
2195 for move in complete:
2196 done_move_ids.append(move.id)
2197 if move.picking_id.id not in ref:
2198 delivery_id = delivery_obj.create(cr, uid, {
2199 'partner_id': partner_id,
2200 'address_id': address_id,
2201 'date': delivery_date,
2202 'name' : move.picking_id.name,
2203 'picking_id': move.picking_id.id
2205 ref[move.picking_id.id] = delivery_id
2206 delivery_obj.write(cr, uid, ref[move.picking_id.id], {
2207 'move_delivered' : [(4,move.id)]
2209 return done_move_ids
2214 class stock_inventory(osv.osv):
2215 _name = "stock.inventory"
2216 _description = "Inventory"
2218 'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
2219 'date': fields.datetime('Date create', required=True, readonly=True, states={'draft': [('readonly', False)]}),
2220 'date_done': fields.datetime('Date done'),
2221 'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', states={'done': [('readonly', True)]}),
2222 'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
2223 'state': fields.selection( (('draft', 'Draft'), ('done', 'Done'), ('cancel','Cancelled')), 'State', readonly=True),
2224 'company_id': fields.many2one('res.company','Company',required=True,select=1),
2227 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
2228 'state': lambda *a: 'draft',
2229 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c)
2233 def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2234 """ Creates a stock move from an inventory line
2235 @param inventory_line:
2239 return self.pool.get('stock.move').create(cr, uid, move_vals)
2241 def action_done(self, cr, uid, ids, context=None):
2242 """ Finishes the inventory and writes its finished date
2245 for inv in self.browse(cr, uid, ids):
2248 for line in inv.inventory_line_id:
2249 pid = line.product_id.id
2251 # price = line.product_id.standard_price or 0.0
2252 amount = self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
2253 change = line.product_qty - amount
2254 lot_id = line.prod_lot_id.id
2256 location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
2258 'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
2259 'product_id': line.product_id.id,
2260 'product_uom': line.product_uom.id,
2261 'prodlot_id': lot_id,
2263 'date_planned': inv.date,
2268 'product_qty': change,
2269 'location_id': location_id,
2270 'location_dest_id': line.location_id.id,
2274 'product_qty': -change,
2275 'location_id': line.location_id.id,
2276 'location_dest_id': location_id,
2280 'prodlot_id': lot_id,
2281 'product_qty': line.product_qty
2283 move_ids.append(self._inventory_line_hook(cr, uid, line, value))
2285 self.pool.get('stock.move').action_done(cr, uid, move_ids,
2287 self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6, 0, move_ids)]})
2290 def action_cancel(self, cr, uid, ids, context={}):
2291 """ Cancels the stock move and change inventory state to draft.
2294 for inv in self.browse(cr, uid, ids):
2295 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
2296 self.write(cr, uid, [inv.id], {'state': 'draft'})
2299 def action_cancel_inventary(self, cr, uid, ids, context={}):
2300 """ Cancels both stock move and inventory
2303 for inv in self.browse(cr,uid,ids):
2304 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
2305 self.write(cr, uid, [inv.id], {'state':'cancel'})
2311 class stock_inventory_line(osv.osv):
2312 _name = "stock.inventory.line"
2313 _description = "Inventory Line"
2315 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2316 'location_id': fields.many2one('stock.location', 'Location', required=True),
2317 'product_id': fields.many2one('product.product', 'Product', required=True),
2318 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
2319 'product_qty': fields.float('Quantity'),
2320 'company_id': fields.related('inventory_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
2321 'prod_lot_id': fields.many2one('stock.production.lot', 'Production Lot', domain="[('product_id','=',product_id)]"),
2322 'state': fields.related('inventory_id','state',type='char',string='State',readonly=True),
2325 def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
2326 """ Changes UoM and name if product_id changes.
2327 @param location_id: Location id
2328 @param product: Changed product_id
2329 @param uom: UoM product
2330 @return: Dictionary of changed values
2335 prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
2336 uom = prod.uom_id.id
2337 amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
2338 result = {'product_qty': amount, 'product_uom': uom}
2339 return {'value': result}
2341 stock_inventory_line()
2344 #----------------------------------------------------------
2346 #----------------------------------------------------------
2347 class stock_warehouse(osv.osv):
2348 _name = "stock.warehouse"
2349 _description = "Warehouse"
2351 'name': fields.char('Name', size=60, required=True),
2352 # 'partner_id': fields.many2one('res.partner', 'Owner'),
2353 'company_id': fields.many2one('res.company','Company',required=True,select=1),
2354 'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
2355 'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True, domain=[('usage','<>','view')]),
2356 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage','<>','view')]),
2357 'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True, domain=[('usage','<>','view')]),
2360 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2366 # get confirm or assign stock move lines of partner and put in current picking.
2367 class stock_picking_move_wizard(osv.osv_memory):
2368 _name = 'stock.picking.move.wizard'
2370 def _get_picking(self, cr, uid, ctx):
2371 if ctx.get('action_id', False):
2372 return ctx['action_id']
2375 def _get_picking_address(self, cr, uid, ctx):
2376 picking_obj = self.pool.get('stock.picking')
2377 if ctx.get('action_id', False):
2378 picking = picking_obj.browse(cr, uid, [ctx['action_id']])[0]
2379 return picking.address_id and picking.address_id.id or False
2383 'name': fields.char('Name', size=64, invisible=True),
2384 #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
2385 'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Entry lines', required=True),
2386 'address_id': fields.many2one('res.partner.address', 'Dest. Address', invisible=True),
2387 'picking_id': fields.many2one('stock.picking', 'Picking list', select=True, invisible=True),
2390 'picking_id': _get_picking,
2391 'address_id': _get_picking_address,
2394 def action_move(self, cr, uid, ids, context=None):
2395 move_obj = self.pool.get('stock.move')
2396 picking_obj = self.pool.get('stock.picking')
2397 account_move_obj = self.pool.get('account.move')
2398 for act in self.read(cr, uid, ids):
2399 move_lines = move_obj.browse(cr, uid, act['move_ids'])
2400 for line in move_lines:
2402 picking_obj.write(cr, uid, [line.picking_id.id], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
2403 picking_obj.write(cr, uid, [act['picking_id']], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
2404 old_picking = picking_obj.read(cr, uid, [line.picking_id.id])[0]
2405 if not len(old_picking['move_lines']):
2406 picking_obj.write(cr, uid, [old_picking['id']], {'state': 'done'})
2408 raise osv.except_osv(_('UserError'),
2409 _('You can not create new moves.'))
2410 return {'type': 'ir.actions.act_window_close'}
2412 stock_picking_move_wizard()
2414 class report_products_to_received_planned(osv.osv):
2415 _name = "report.products.to.received.planned"
2416 _description = "Product to Received Vs Planned"
2419 'date':fields.date('Date'),
2420 'qty': fields.integer('Actual Qty'),
2421 'planned_qty': fields.integer('Planned Qty'),
2426 tools.drop_view_if_exists(cr, 'report_products_to_received_planned')
2428 create or replace view report_products_to_received_planned as (
2429 select stock.date, min(stock.id) as id, sum(stock.product_qty) as qty, 0 as planned_qty
2430 from stock_picking picking
2431 inner join stock_move stock
2432 on picking.id = stock.picking_id and picking.type = 'in'
2433 where stock.date between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2438 select stock.date_planned, min(stock.id) as id, 0 as actual_qty, sum(stock.product_qty) as planned_qty
2439 from stock_picking picking
2440 inner join stock_move stock
2441 on picking.id = stock.picking_id and picking.type = 'in'
2442 where stock.date_planned between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2443 group by stock.date_planned
2446 report_products_to_received_planned()
2448 class report_delivery_products_planned(osv.osv):
2449 _name = "report.delivery.products.planned"
2450 _description = "Number of Delivery products vs planned"
2453 'date':fields.date('Date'),
2454 'qty': fields.integer('Actual Qty'),
2455 'planned_qty': fields.integer('Planned Qty'),
2460 tools.drop_view_if_exists(cr, 'report_delivery_products_planned')
2462 create or replace view report_delivery_products_planned as (
2463 select stock.date, min(stock.id) as id, sum(stock.product_qty) as qty, 0 as planned_qty
2464 from stock_picking picking
2465 inner join stock_move stock
2466 on picking.id = stock.picking_id and picking.type = 'out'
2467 where stock.date between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2472 select stock.date_planned, min(stock.id), 0 as actual_qty, sum(stock.product_qty) as planned_qty
2473 from stock_picking picking
2474 inner join stock_move stock
2475 on picking.id = stock.picking_id and picking.type = 'out'
2476 where stock.date_planned between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2477 group by stock.date_planned
2482 report_delivery_products_planned()
2483 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: