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 osv import osv, fields
27 from _common import rounding
29 from tools import config
34 #----------------------------------------------------------
36 #----------------------------------------------------------
38 class product_uom_categ(osv.osv):
39 _name = 'product.uom.categ'
40 _description = 'Product uom categ'
42 'name': fields.char('Name', size=64, required=True, translate=True),
46 class product_uom(osv.osv):
48 _description = 'Product Unit of Measure'
50 def _factor(self, cursor, user, ids, name, arg, context):
52 for uom in self.browse(cursor, user, ids, context=context):
54 if uom.factor_inv_data:
55 res[uom.id] = uom.factor_inv_data
57 res[uom.id] = round(1 / uom.factor, 6)
62 def _factor_inv(self, cursor, user, id, name, value, arg, context):
64 if 'read_delta' in ctx:
68 if round(1 / round(1/value, 6), 6) != value:
70 self.write(cursor, user, id, {
71 'factor': round(1/value, 6),
72 'factor_inv_data': data,
75 self.write(cursor, user, id, {
77 'factor_inv_data': 0.0,
81 'name': fields.char('Name', size=64, required=True, translate=True),
82 'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
83 help="Unit of Measure of the same category can be converted between each others."),
84 'factor': fields.float('Rate', digits=(12, 6), required=True,
85 help='The coefficient for the formula:\n' \
86 '1 (base unit) = coef (this unit). Rate = 1 / Factor.'),
87 'factor_inv': fields.function(_factor, fnct_inv=_factor_inv, digits=(12, 6),
88 method=True, string='Factor',
89 help='The coefficient for the formula:\n' \
90 'coef (base unit) = 1 (this unit). Factor = 1 / Rate.'),
91 'factor_inv_data': fields.float('Factor', digits=(12, 6)),
92 'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
93 help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be splitted."),
94 'active': fields.boolean('Active'),
98 'factor': lambda *a: 1.0,
99 'factor_inv': lambda *a: 1.0,
100 'active': lambda *a: 1,
101 'rounding': lambda *a: 0.01,
104 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
105 if not from_uom_id or not qty or not to_uom_id:
107 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
108 if uoms[0].id == from_uom_id:
109 from_unit, to_unit = uoms[0], uoms[-1]
111 from_unit, to_unit = uoms[-1], uoms[0]
112 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
114 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context={}):
115 if from_unit.category_id.id <> to_unit.category_id.id:
117 if from_unit.factor_inv_data:
118 amount = qty * from_unit.factor_inv_data
120 amount = qty / from_unit.factor
122 if to_unit.factor_inv_data:
123 amount = rounding(amount / to_unit.factor_inv_data, to_unit.rounding)
125 amount = rounding(amount * to_unit.factor, to_unit.rounding)
128 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
129 if not from_uom_id or not price or not to_uom_id:
131 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
132 if uoms[0].id == from_uom_id:
133 from_unit, to_unit = uoms[0], uoms[-1]
135 from_unit, to_unit = uoms[-1], uoms[0]
136 if from_unit.category_id.id <> to_unit.category_id.id:
138 if from_unit.factor_inv_data:
139 amount = price / from_unit.factor_inv_data
141 amount = price * from_unit.factor
143 if to_unit.factor_inv_data:
144 amount = amount * to_unit.factor_inv_data
146 amount = amount / to_unit.factor
149 def onchange_factor_inv(self, cursor, user, ids, value):
151 return {'value': {'factor': 0}}
152 return {'value': {'factor': round(1/value, 6)}}
154 def onchange_factor(self, cursor, user, ids, value):
156 return {'value': {'factor_inv': 0}}
157 return {'value': {'factor_inv': round(1/value, 6)}}
162 class product_ul(osv.osv):
164 _description = "Shipping Unit"
166 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
167 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Palet')], 'Type', required=True),
172 #----------------------------------------------------------
174 #----------------------------------------------------------
175 class product_category(osv.osv):
177 def name_get(self, cr, uid, ids, context=None):
180 reads = self.read(cr, uid, ids, ['name','parent_id'], context)
183 name = record['name']
184 if record['parent_id']:
185 name = record['parent_id'][1]+' / '+name
186 res.append((record['id'], name))
189 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
190 res = self.name_get(cr, uid, ids, context)
193 _name = "product.category"
194 _description = "Product Category"
196 'name': fields.char('Name', size=64, required=True, translate=True),
197 'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
198 'parent_id': fields.many2one('product.category','Parent Category', select=True),
199 'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories'),
200 'sequence': fields.integer('Sequence'),
203 def _check_recursion(self, cr, uid, ids):
206 cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
207 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
214 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
216 def child_get(self, cr, uid, ids):
222 #----------------------------------------------------------
224 #----------------------------------------------------------
225 class product_template(osv.osv):
226 _name = "product.template"
227 _description = "Product Template"
228 def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
230 for product in self.browse(cr, uid, ids, context):
231 if product.seller_ids:
232 result[product.id] = product.seller_ids[0].delay
234 result[product.id] = 1
238 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
239 'product_manager': fields.many2one('res.users','Product Manager'),
240 'description': fields.text('Description',translate=True),
241 'description_purchase': fields.text('Purchase Description',translate=True),
242 'description_sale': fields.text('Sale Description',translate=True),
243 'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed, consumable are stockable products with infinite stock, or without a stock management in the system."),
244 'supply_method': fields.selection([('produce','Produce'),('buy','Buy')], 'Supply method', required=True, help="Produce will generate production order or tasks, according to the product type. Purchase will trigger purchase orders when requested."),
245 'sale_delay': fields.float('Customer Lead Time', help="This is the average time between the confirmation of the customer order and the delivery of the finnished products. It's the time you promise to your customers."),
246 'produce_delay': fields.float('Manufacturing Lead Time', help="Average time to produce this product. This is only for the production order and, if it is a multi-level bill of material, it's only for the level of this product. Different delays will be summed for all levels and purchase orders."),
247 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procure Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until refurnishing. 'Make to Order': When needed, purchase or produce for the procurement request."),
248 'rental': fields.boolean('Rentable product'),
249 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
250 'list_price': fields.float('Sale Price', digits=(16, int(config['price_accuracy'])), help="Base price for computing the customer price. Sometimes called the catalog price."),
251 'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy'])), help="The cost of the product for accounting stock valorisation. It can serves as a base price for supplier price."),
252 'volume': fields.float('Volume', help="The volume in m3."),
253 'weight': fields.float('Gross weight', help="The gross weight in Kg."),
254 'weight_net': fields.float('Net weight', help="The net weight in Kg."),
255 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
256 help="Standard Price: the cost price is fixed and recomputed periodically (usually at the end of the year), Average Price: the cost price is recomputed at each reception of products."),
257 'warranty': fields.float('Warranty (months)'),
258 'sale_ok': fields.boolean('Can be sold', help="Determine if the product can be visible in the list of product within a selection from a sale order line."),
259 'purchase_ok': fields.boolean('Can be Purchased', help="Determine if the product is visible in the list of products within a selection from a purchase order line."),
260 'state': fields.selection([('',''),('draft', 'In Development'),('sellable','In Production'),('end','End of Lifecycle'),('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
261 'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
262 'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True, help="Default Unit of Measure used for purchase orders. It must in the same category than the default unit of measure."),
263 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
264 help='Used by companies that manages two unit of measure: invoicing and stock management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
265 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
266 help='Coefficient to convert UOM to UOS\n'
267 ' uom = uos * coeff'),
268 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
269 'seller_delay': fields.function(_calc_seller_delay, method=True, type='integer', string='Supplier Lead Time', help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
270 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
271 'loc_rack': fields.char('Rack', size=16),
272 'loc_row': fields.char('Row', size=16),
273 'loc_case': fields.char('Case', size=16),
274 'company_id': fields.many2one('res.company', 'Company'),
277 def _get_uom_id(self, cr, uid, *args):
278 cr.execute('select id from product_uom order by id limit 1')
280 return res and res[0] or False
282 def _default_category(self, cr, uid, context={}):
283 if 'categ_id' in context and context['categ_id']:
284 return context['categ_id']
287 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
288 if uom_id and uom_po_id:
289 uom_obj=self.pool.get('product.uom')
290 uom=uom_obj.browse(cursor,user,[uom_id])[0]
291 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
292 if uom.category_id.id != uom_po.category_id.id:
293 return {'value': {'uom_po_id': uom_id}}
297 'company_id': lambda self, cr, uid, context: \
298 self.pool.get('res.users').browse(cr, uid, uid,
299 context=context).company_id.id,
300 'type': lambda *a: 'product',
301 'list_price': lambda *a: 1,
302 'cost_method': lambda *a: 'standard',
303 'supply_method': lambda *a: 'buy',
304 'standard_price': lambda *a: 1,
305 'sale_ok': lambda *a: 1,
306 'sale_delay': lambda *a: 7,
307 'produce_delay': lambda *a: 1,
308 'purchase_ok': lambda *a: 1,
309 'procure_method': lambda *a: 'make_to_stock',
310 'uom_id': _get_uom_id,
311 'uom_po_id': _get_uom_id,
312 'uos_coeff' : lambda *a: 1.0,
313 'mes_type' : lambda *a: 'fixed',
314 'categ_id' : _default_category,
317 def _check_uom(self, cursor, user, ids):
318 for product in self.browse(cursor, user, ids):
319 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
323 def _check_uos(self, cursor, user, ids):
324 for product in self.browse(cursor, user, ids):
326 and product.uos_id.category_id.id \
327 == product.uom_id.category_id.id:
332 (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
333 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
336 def name_get(self, cr, user, ids, context={}):
337 if 'partner_id' in context:
339 return super(product_template, self).name_get(cr, user, ids, context)
343 class product_product(osv.osv):
344 def view_header_get(self, cr, uid, view_id, view_type, context):
345 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
346 if (context.get('categ_id', False)):
347 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
350 def _product_price(self, cr, uid, ids, name, arg, context={}):
352 quantity = context.get('quantity', 1)
353 pricelist = context.get('pricelist', False)
357 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
362 res.setdefault(id, 0.0)
365 def _get_product_available_func(states, what):
366 def _product_available(self, cr, uid, ids, name, arg, context={}):
367 return {}.fromkeys(ids, 0.0)
368 return _product_available
370 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
371 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
372 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
373 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
375 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
377 product_uom_obj = self.pool.get('product.uom')
379 res.setdefault(id, 0.0)
380 for product in self.browse(cr, uid, ids, context=context):
382 uom = product.uos_id or product.uom_id
383 res[product.id] = product_uom_obj._compute_price(cr, uid,
384 uom.id, product.list_price, context['uom'])
386 res[product.id] = product.list_price
389 def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
390 product = self.browse(cr, uid, [product_id], context)[0]
391 for supinfo in product.seller_ids:
392 if supinfo.name.id == partner_id:
393 return {'code': supinfo.product_code, 'name': supinfo.product_name}
394 return {'code' : product.default_code, 'name' : product.name}
396 def _product_code(self, cr, uid, ids, name, arg, context={}):
398 for p in self.browse(cr, uid, ids, context):
399 res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
402 def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
404 for p in self.browse(cr, uid, ids, context):
405 data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
407 data['name'] = p.code
409 data['name'] = p.name
410 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
415 'active': lambda *a: 1,
416 'price_extra': lambda *a: 0.0,
417 'price_margin': lambda *a: 1.0,
420 _name = "product.product"
421 _description = "Product"
422 _table = "product_product"
423 _inherits = {'product.template': 'product_tmpl_id'}
425 'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
426 'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
427 'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
428 'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
429 'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
430 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
431 'code': fields.function(_product_code, method=True, type='char', string='Code'),
432 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
433 'default_code' : fields.char('Code', size=64),
434 'active': fields.boolean('Active'),
435 'variants': fields.char('Variants', size=64),
436 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
437 'ean13': fields.char('EAN13', size=13),
438 'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the packing order and is mainly used if you use the EDI module."),
439 'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
440 'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
443 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
444 if uom_id and uom_po_id:
445 uom_obj=self.pool.get('product.uom')
446 uom=uom_obj.browse(cursor,user,[uom_id])[0]
447 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
448 if uom.category_id.id != uom_po.category_id.id:
449 return {'value': {'uom_po_id': uom_id}}
452 def _check_ean_key(self, cr, uid, ids):
453 for partner in self.browse(cr, uid, ids):
454 if not partner.ean13:
456 if len(partner.ean13) <> 13:
465 sum += int(partner.ean13[i])
467 sum += 3 * int(partner.ean13[i])
468 check = int(math.ceil(sum / 10.0) * 10 - sum)
469 if check != int(partner.ean13[12]):
473 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
475 def on_order(self, cr, uid, ids, orderline, quantity):
478 def name_get(self, cr, user, ids, context={}):
482 #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
483 #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
484 name = d.get('name','')
485 code = d.get('default_code',False)
487 name = '[%s] %s' % (code,name)
489 name = name + ' - %s' % (d['variants'],)
490 return (d['id'], name)
491 result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
494 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
499 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
501 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
503 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
504 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
505 result = self.name_get(cr, user, ids, context)
509 # Could be overrided for variants matrices prices
511 def price_get(self, cr, uid, ids, ptype='list_price', context={}):
513 product_uom_obj = self.pool.get('product.uom')
515 for product in self.browse(cr, uid, ids, context=context):
516 res[product.id] = product[ptype] or 0.0
517 if ptype == 'list_price':
518 res[product.id] = (res[product.id] * product.price_margin) + \
521 uom = product.uos_id or product.uom_id
522 res[product.id] = product_uom_obj._compute_price(cr, uid,
523 uom.id, res[product.id], context['uom'])
526 def copy(self, cr, uid, id, default=None, context=None):
530 if ('variant' in context) and context['variant']:
531 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
532 'price_margin', 'price_extra']
533 data = self.read(cr, uid, id, fields=fields, context=context)
537 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
538 and data['product_tmpl_id'][0]
540 return self.create(cr, uid, data)
542 return super(product_product, self).copy(cr, uid, id, default=default,
546 class product_packaging(osv.osv):
547 _name = "product.packaging"
548 _description = "Packaging"
551 'sequence': fields.integer('Sequence'),
552 'name' : fields.char('Description', size=64),
553 'qty' : fields.float('Quantity by Package',
554 help="The total number of products you can put by palet or box."),
555 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
556 'ul_qty' : fields.integer('Package by layer'),
557 'rows' : fields.integer('Number of Layer', required=True,
558 help='The number of layer on a palet or box'),
559 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
560 'ean' : fields.char('EAN', size=14,
561 help="The EAN code of the package unit."),
562 'code' : fields.char('Code', size=14,
563 help="The code of the transport unit."),
564 'weight': fields.float('Total Package Weight',
565 help='The weight of a full of products palet or box.'),
566 'weight_ul': fields.float('Empty Package Weight',
567 help='The weight of the empty UL'),
568 'height': fields.float('Height', help='The height of the package'),
569 'width': fields.float('Width', help='The width of the package'),
570 'length': fields.float('Length', help='The length of the package'),
573 def _get_1st_ul(self, cr, uid, context={}):
574 cr.execute('select id from product_ul order by id asc limit 1')
576 return (res and res[0]) or False
579 'rows' : lambda *a : 3,
580 'sequence' : lambda *a : 1,
585 salt = '31' * 6 + '3'
587 for ean_part, salt_part in zip(ean, salt):
588 sum += int(ean_part) * int(salt_part)
589 return (10 - (sum % 10)) % 10
590 checksum = staticmethod(checksum)
595 class product_supplierinfo(osv.osv):
596 _name = "product.supplierinfo"
597 _description = "Information about a product supplier"
599 'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
600 'product_name': fields.char('Partner Product Name', size=128, help="Name of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
601 'product_code': fields.char('Partner Product Code', size=64, help="Code of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
602 'sequence' : fields.integer('Priority'),
603 'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
604 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
605 'delay' : fields.integer('Delivery Delay', required=True, help="Delay in days between the confirmation of the purchase order and the reception of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
606 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
609 'qty': lambda *a: 0.0,
610 'sequence': lambda *a: 1,
611 'delay': lambda *a: 1,
614 product_supplierinfo()
617 class pricelist_partnerinfo(osv.osv):
618 _name = 'pricelist.partnerinfo'
620 'name': fields.char('Description', size=64),
621 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
622 'min_quantity': fields.float('Quantity', required=True),
623 'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
625 _order = 'min_quantity asc'
626 pricelist_partnerinfo()
630 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: