1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ##############################################################################
31 from osv import osv, fields
35 from _common import rounding
37 from tools import config
42 #----------------------------------------------------------
44 #----------------------------------------------------------
46 class product_uom_categ(osv.osv):
47 _name = 'product.uom.categ'
48 _description = 'Product uom categ'
50 'name': fields.char('Name', size=64, required=True),
54 class product_uom(osv.osv):
56 _description = 'Product Unit of Measure'
58 def _factor(self, cursor, user, ids, name, arg, context):
60 for uom in self.browse(cursor, user, ids, context=context):
62 if uom.factor_inv_data:
63 res[uom.id] = uom.factor_inv_data
65 res[uom.id] = round(1 / uom.factor, 6)
70 def _factor_inv(self, cursor, user, id, name, value, arg, context):
72 if 'read_delta' in ctx:
76 if round(1 / round(1/value, 6), 6) != value:
78 self.write(cursor, user, id, {
79 'factor': round(1/value, 6),
80 'factor_inv_data': data,
83 self.write(cursor, user, id, {
85 'factor_inv_data': 0.0,
89 'name': fields.char('Name', size=64, required=True),
90 'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
91 help="Unit of Measure of the same category can be converted between each others."),
92 'factor': fields.float('Rate', digits=(12, 6), required=True,
93 help='The coefficient for the formula:\n' \
94 '1 (base unit) = coef (this unit)'),
95 'factor_inv': fields.function(_factor, fnct_inv=_factor_inv, digits=(12, 6),
96 method=True, string='Factor',
97 help='The coefficient for the formula:\n' \
98 'coef (base unit) = 1 (this unit)'),
99 'factor_inv_data': fields.float('Factor', digits=(12, 6)),
100 'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
101 help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be splitted."),
102 'active': fields.boolean('Active'),
106 'factor': lambda *a: 1.0,
107 'factor_inv': lambda *a: 1.0,
108 'active': lambda *a: 1,
109 'rounding': lambda *a: 0.01,
112 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
113 if not from_uom_id or not qty or not to_uom_id:
115 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
116 if uoms[0].id == from_uom_id:
117 from_unit, to_unit = uoms[0], uoms[-1]
119 from_unit, to_unit = uoms[-1], uoms[0]
120 if from_unit.category_id.id <> to_unit.category_id.id:
122 if from_unit['factor_inv_data']:
123 amount = qty * from_unit['factor_inv_data']
125 amount = qty / from_unit['factor']
127 if to_unit['factor_inv_data']:
128 amount = rounding(amount / to_unit['factor_inv_data'], to_unit['rounding'])
130 amount = rounding(amount * to_unit['factor'], to_unit['rounding'])
133 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
134 if not from_uom_id or not price or not to_uom_id:
136 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
137 if uoms[0].id == from_uom_id:
138 from_unit, to_unit = uoms[0], uoms[-1]
140 from_unit, to_unit = uoms[-1], uoms[0]
141 if from_unit.category_id.id <> to_unit.category_id.id:
143 if from_unit.factor_inv_data:
144 amount = price / from_unit.factor_inv_data
146 amount = price * from_unit.factor
148 if to_unit.factor_inv_data:
149 amount = amount * to_unit.factor_inv_data
151 amount = amount / to_unit.factor
154 def onchange_factor_inv(self, cursor, user, ids, value):
156 return {'value': {'factor': 0}}
157 return {'value': {'factor': round(1/value, 6)}}
159 def onchange_factor(self, cursor, user, ids, value):
161 return {'value': {'factor_inv': 0}}
162 return {'value': {'factor_inv': round(1/value, 6)}}
167 class product_ul(osv.osv):
169 _description = "Shipping Unit"
171 'name' : fields.char('Name', size=64,select=True),
172 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Palet')], 'Type', required=True),
177 #----------------------------------------------------------
179 #----------------------------------------------------------
180 class product_category(osv.osv):
182 def name_get(self, cr, uid, ids, context={}):
185 reads = self.read(cr, uid, ids, ['name','parent_id'], context)
188 name = record['name']
189 if record['parent_id']:
190 name = record['parent_id'][1]+' / '+name
191 res.append((record['id'], name))
194 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
195 res = self.name_get(cr, uid, ids)
198 _name = "product.category"
199 _description = "Product Category"
201 'name': fields.char('Name', size=64, required=True, translate=True),
202 'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
203 'parent_id': fields.many2one('product.category','Parent Category', select=True),
204 'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories'),
205 'sequence': fields.integer('Sequence'),
208 def _check_recursion(self, cr, uid, ids):
211 cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
212 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
219 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
221 def child_get(self, cr, uid, ids):
227 #----------------------------------------------------------
229 #----------------------------------------------------------
230 class product_template(osv.osv):
231 _name = "product.template"
232 _description = "Product Template"
233 def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
235 for product in self.browse(cr, uid, ids, context):
236 if product.seller_ids:
237 result[product.id] = product.seller_ids[0].delay
239 result[product.id] = 1
243 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
244 'product_manager': fields.many2one('res.users','Product Manager'),
245 'description': fields.text('Description',translate=True),
246 'description_purchase': fields.text('Purchase Description',translate=True),
247 'description_sale': fields.text('Sale Description',translate=True),
248 '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."),
249 '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."),
250 '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."),
251 '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."),
252 '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."),
253 'rental': fields.boolean('Rentable product'),
254 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
255 '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."),
256 '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."),
257 'volume': fields.float('Volume', help="The weight in Kg."),
258 'weight': fields.float('Gross weight'),
259 'weight_net': fields.float('Net weight'),
260 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
261 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."),
262 'warranty': fields.float('Warranty (months)'),
263 '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."),
264 '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."),
265 'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
266 '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."),
267 '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."),
268 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
269 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.'),
270 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
271 help='Coefficient to convert UOM to UOS\n'
272 ' uom = uos * coeff'),
273 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
274 '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."),
275 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
276 'loc_rack': fields.char('Rack', size=16),
277 'loc_row': fields.char('Row', size=16),
278 'loc_case': fields.char('Case', size=16),
281 def _get_uom_id(self, cr, uid, *args):
282 cr.execute('select id from product_uom order by id limit 1')
284 return res and res[0] or False
287 'type': lambda *a: 'product',
288 'list_price': lambda *a: 1,
289 'cost_method': lambda *a: 'standard',
290 'supply_method': lambda *a: 'buy',
291 'standard_price': lambda *a: 1,
292 'sale_ok': lambda *a: 1,
293 'sale_delay': lambda *a: 7,
294 'produce_delay': lambda *a: 1,
295 'purchase_ok': lambda *a: 1,
296 'procure_method': lambda *a: 'make_to_stock',
297 'uom_id': _get_uom_id,
298 'uom_po_id': _get_uom_id,
299 'uos_coeff' : lambda *a: 1.0,
300 'mes_type' : lambda *a: 'fixed',
303 def _check_uom(self, cursor, user, ids):
304 for product in self.browse(cursor, user, ids):
305 if product.uom_id.id <> product.uom_po_id.id:
309 def _check_uos(self, cursor, user, ids):
310 for product in self.browse(cursor, user, ids):
312 and product.uos_id.category_id.id \
313 == product.uom_id.category_id.id:
318 (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
319 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
322 def name_get(self, cr, user, ids, context={}):
323 if 'partner_id' in context:
325 return super(product_template, self).name_get(cr, user, ids, context)
329 class product_product(osv.osv):
330 def view_header_get(self, cr, uid, view_id, view_type, context):
331 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
332 if (context.get('categ_id', False)):
333 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
336 def _product_price(self, cr, uid, ids, name, arg, context={}):
338 quantity = context.get('quantity', 1)
339 pricelist = context.get('pricelist', False)
342 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
345 res.setdefault(id, 0.0)
348 def _get_product_available_func(states, what):
349 def _product_available(self, cr, uid, ids, name, arg, context={}):
350 return {}.fromkeys(ids, 0.0)
351 return _product_available
353 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
354 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
355 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
356 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
358 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
360 product_uom_obj = self.pool.get('product.uom')
362 res.setdefault(id, 0.0)
363 for product in self.browse(cr, uid, ids, context=context):
365 uom = product.uos_id or product.uom_id
366 res[product.id] = product_uom_obj._compute_price(cr, uid,
367 uom.id, product.list_price, context['uom'])
369 res[product.id] = product.list_price
372 def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
373 product = self.browse(cr, uid, [product_id], context)[0]
374 for supinfo in product.seller_ids:
375 if supinfo.name.id == partner_id:
376 return {'code': supinfo.product_code, 'name': supinfo.product_name}
377 return {'code' : product.default_code, 'name' : product.name}
379 def _product_code(self, cr, uid, ids, name, arg, context={}):
381 for p in self.browse(cr, uid, ids, context):
382 res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
385 def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
387 for p in self.browse(cr, uid, ids, context):
388 data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
390 data['name'] = p.code
392 data['name'] = p.name
393 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
398 'active': lambda *a: 1,
399 'price_extra': lambda *a: 0.0,
400 'price_margin': lambda *a: 1.0,
403 _name = "product.product"
404 _description = "Product"
405 _table = "product_product"
406 _inherits = {'product.template': 'product_tmpl_id'}
408 'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
409 'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
410 'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
411 'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
412 'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
413 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
414 'code': fields.function(_product_code, method=True, type='char', string='Code'),
415 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
416 'default_code' : fields.char('Code', size=64),
417 'active': fields.boolean('Active'),
418 'variants': fields.char('Variants', size=64),
419 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
420 'ean13': fields.char('EAN13', size=13),
421 '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."),
422 'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
423 'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
426 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
427 if uom_id and uom_po_id:
428 uom_obj=self.pool.get('product.uom')
429 uom=uom_obj.browse(cursor,user,[uom_id])[0]
430 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
431 if uom.category_id.id != uom_po.category_id.id:
432 return {'value': {'uom_po_id': uom_id}}
435 def _check_ean_key(self, cr, uid, ids):
436 for partner in self.browse(cr, uid, ids):
437 if not partner.ean13:
439 if len(partner.ean13) < 12:
448 sum += int(partner.ean13[i])
450 sum += 3 * int(partner.ean13[i])
451 check = int(math.ceil(sum / 10.0) * 10 - sum)
452 if len(partner.ean13) == 12:
453 self.write(cr, uid, partner.id, {
454 'ean13': partner.ean13 + str(check)
456 elif check != int(partner.ean13[12]):
460 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
462 def on_order(self, cr, uid, ids, orderline, quantity):
465 def name_get(self, cr, user, ids, context={}):
469 #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
470 #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
471 name = d.get('name','')
472 code = d.get('default_code',False)
474 name = '[%s] %s' % (code,name)
476 name = name + ' - %s' % (d['variants'],)
477 return (d['id'], name)
478 result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
481 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
486 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
488 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
490 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
491 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
492 result = self.name_get(cr, user, ids, context)
496 # Could be overrided for variants matrices prices
498 def price_get(self, cr, uid, ids, ptype='list_price', context={}):
500 product_uom_obj = self.pool.get('product.uom')
502 for product in self.browse(cr, uid, ids, context=context):
503 res[product.id] = product[ptype] or 0.0
504 if ptype == 'list_price':
505 res[product.id] = (res[product.id] * product.price_margin) + \
508 uom = product.uos_id or product.uom_id
509 res[product.id] = product_uom_obj._compute_price(cr, uid,
510 uom.id, res[product.id], context['uom'])
513 def copy(self, cr, uid, id, default=None, context=None):
517 if ('variant' in context) and context['variant']:
518 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
519 'price_margin', 'price_extra']
520 data = self.read(cr, uid, id, fields=fields, context=context)
524 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
525 and data['product_tmpl_id'][0]
527 return self.create(cr, uid, data)
529 return super(product_product, self).copy(cr, uid, id, default=default,
533 class product_packaging(osv.osv):
534 _name = "product.packaging"
535 _description = "Packaging"
538 'sequence': fields.integer('Sequence'),
539 'name' : fields.char('Description', size=64),
540 'qty' : fields.float('Quantity by Package',
541 help="The total number of products you can put by palet or box."),
542 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
543 'ul_qty' : fields.integer('Package by layer'),
544 'rows' : fields.integer('Number of Layer', required=True,
545 help='The number of layer on a palet or box'),
546 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
547 'ean' : fields.char('EAN', size=14,
548 help="The EAN code of the package unit."),
549 'code' : fields.char('Code', size=14,
550 help="The code of the transport unit."),
551 'weight': fields.float('Total Package Weight',
552 help='The weight of a full of products palet or box.'),
553 'weight_ul': fields.float('Empty Package Weight',
554 help='The weight of the empty UL'),
555 'height': fields.float('Height', help='The height of the package'),
556 'width': fields.float('Width', help='The width of the package'),
557 'length': fields.float('Length', help='The length of the package'),
560 def _get_1st_ul(self, cr, uid, context={}):
561 cr.execute('select id from product_ul order by id asc limit 1')
563 return (res and res[0]) or False
566 'rows' : lambda *a : 3,
567 'sequence' : lambda *a : 1,
572 salt = '31' * 6 + '3'
574 for ean_part, salt_part in zip(ean, salt):
575 sum += int(ean_part) * int(salt_part)
576 return (10 - (sum % 10)) % 10
577 checksum = staticmethod(checksum)
582 class product_supplierinfo(osv.osv):
583 _name = "product.supplierinfo"
584 _description = "Information about a product supplier"
586 'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
587 '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."),
588 '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."),
589 'sequence' : fields.integer('Priority'),
590 'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
591 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
592 '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."),
593 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
596 'qty': lambda *a: 0.0,
597 'sequence': lambda *a: 1,
598 'delay': lambda *a: 1,
601 product_supplierinfo()
604 class pricelist_partnerinfo(osv.osv):
605 _name = 'pricelist.partnerinfo'
607 'name': fields.char('Description', size=64),
608 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
609 'min_quantity': fields.float('Quantity', required=True),
610 'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
612 _order = 'min_quantity asc'
613 pricelist_partnerinfo()
617 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: