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 'factor': fields.float('Rate', digits=(12, 6), required=True,
92 help='The coefficient for the formula:\n' \
93 '1 (base unit) = coef (this unit)'),
94 'factor_inv': fields.function(_factor, fnct_inv=_factor_inv, digits=(12, 6),
95 method=True, string='Factor',
96 help='The coefficient for the formula:\n' \
97 'coef (base unit) = 1 (this unit)'),
98 'factor_inv_data': fields.float('Factor', digits=(12, 6)),
99 'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True),
100 'active': fields.boolean('Active'),
104 'factor': lambda *a: 1.0,
105 'factor_inv': lambda *a: 1.0,
106 'active': lambda *a: 1,
107 'rounding': lambda *a: 0.01,
110 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
111 if not from_uom_id or not qty or not to_uom_id:
113 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
114 if uoms[0].id == from_uom_id:
115 from_unit, to_unit = uoms[0], uoms[-1]
117 from_unit, to_unit = uoms[-1], uoms[0]
118 if from_unit.category_id.id <> to_unit.category_id.id:
120 if from_unit['factor_inv_data']:
121 amount = qty * from_unit['factor_inv_data']
123 amount = qty / from_unit['factor']
125 if to_unit['factor_inv_data']:
126 amount = rounding(amount / to_unit['factor_inv_data'], to_unit['rounding'])
128 amount = rounding(amount * to_unit['factor'], to_unit['rounding'])
131 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
132 if not from_uom_id or not price or not to_uom_id:
134 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
135 if uoms[0].id == from_uom_id:
136 from_unit, to_unit = uoms[0], uoms[-1]
138 from_unit, to_unit = uoms[-1], uoms[0]
139 if from_unit.category_id.id <> to_unit.category_id.id:
141 if from_unit.factor_inv_data:
142 amount = price / from_unit.factor_inv_data
144 amount = price * from_unit.factor
146 if to_unit.factor_inv_data:
147 amount = amount * to_unit.factor_inv_data
149 amount = amount / to_unit.factor
152 def onchange_factor_inv(self, cursor, user, ids, value):
154 return {'value': {'factor': 0}}
155 return {'value': {'factor': round(1/value, 6)}}
157 def onchange_factor(self, cursor, user, ids, value):
159 return {'value': {'factor_inv': 0}}
160 return {'value': {'factor_inv': round(1/value, 6)}}
165 class product_ul(osv.osv):
167 _description = "Shipping Unit"
169 'name' : fields.char('Name', size=64,select=True),
170 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Palet')], 'Type', required=True),
175 #----------------------------------------------------------
177 #----------------------------------------------------------
178 class product_category(osv.osv):
180 def name_get(self, cr, uid, ids, context={}):
183 reads = self.read(cr, uid, ids, ['name','parent_id'], context)
186 name = record['name']
187 if record['parent_id']:
188 name = record['parent_id'][1]+' / '+name
189 res.append((record['id'], name))
192 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
193 res = self.name_get(cr, uid, ids)
196 _name = "product.category"
197 _description = "Product Category"
199 'name': fields.char('Name', size=64, required=True, translate=True),
200 'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
201 'parent_id': fields.many2one('product.category','Parent Category', select=True),
202 'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories'),
203 'sequence': fields.integer('Sequence'),
206 def _check_recursion(self, cr, uid, ids):
209 cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
210 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
217 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
219 def child_get(self, cr, uid, ids):
225 #----------------------------------------------------------
227 #----------------------------------------------------------
228 class product_template(osv.osv):
229 _name = "product.template"
230 _description = "Product Template"
232 def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
234 for product in self.browse(cr, uid, ids, context):
235 if product.seller_ids:
236 result[product.id] = product.seller_ids[0].delay
238 result[product.id] = 1
242 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
243 'product_manager': fields.many2one('res.users','Product Manager'),
244 'description': fields.text('Description'),
245 'description_purchase': fields.text('Purchase Description'),
246 'description_sale': fields.text('Sale Description'),
247 '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."),
248 '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."),
249 '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."),
250 '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."),
251 '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."),
252 'rental': fields.boolean('Rentable product'),
253 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
254 'list_price': fields.float('Sale Price', digits=(16, int(config['price_accuracy']))),
255 'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy']))),
256 'volume': fields.float('Volume', help="The weight in Kg."),
257 'weight': fields.float('Gross weight'),
258 'weight_net': fields.float('Net weight'),
259 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True),
260 'warranty': fields.float('Warranty (months)'),
261 '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."),
262 '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."),
263 'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="This is the default Unit of Measure used for all stock operation."),
264 'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True),
265 '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."),
266 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
267 help='Keep empty to use the default UOM'),
268 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
269 help='Coefficient to convert UOM to UOS\n'
270 ' uom = uos * coeff'),
271 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
272 'tracking': fields.boolean('Track Lots', help="Force to use a Production Lot number during stock operations for traceability."),
273 '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."),
274 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
275 'loc_rack': fields.char('Rack', size=16),
276 'loc_row': fields.char('Row', size=16),
277 'loc_case': fields.char('Case', size=16),
280 def _get_uom_id(self, cr, uid, *args):
281 cr.execute('select id from product_uom order by id limit 1')
283 return res and res[0] or False
286 'type': lambda *a: 'product',
287 'list_price': lambda *a: 1,
288 'cost_method': lambda *a: 'standard',
289 'supply_method': lambda *a: 'buy',
290 'standard_price': lambda *a: 1,
291 'sale_ok': lambda *a: 1,
292 'sale_delay': lambda *a: 7,
293 'produce_delay': lambda *a: 1,
294 'purchase_ok': lambda *a: 1,
295 'procure_method': lambda *a: 'make_to_stock',
296 'uom_id': _get_uom_id,
297 'uom_po_id': _get_uom_id,
298 'uos_coeff' : lambda *a: 1.0,
299 'mes_type' : lambda *a: 'fixed',
302 def _check_uom(self, cursor, user, ids):
303 for product in self.browse(cursor, user, ids):
304 if product.uom_id.id <> product.uom_po_id.id:
308 def _check_uos(self, cursor, user, ids):
309 for product in self.browse(cursor, user, ids):
311 and product.uos_id.category_id.id \
312 == product.uom_id.category_id.id:
317 (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
318 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
321 def name_get(self, cr, user, ids, context={}):
322 if 'partner_id' in context:
324 return super(product_template, self).name_get(cr, user, ids, context)
328 class product_product(osv.osv):
329 def view_header_get(self, cr, uid, view_id, view_type, context):
330 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
332 if (not context.get('categ_id', False)):
334 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)
389 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
394 'active': lambda *a: 1,
395 'price_extra': lambda *a: 0.0,
396 'price_margin': lambda *a: 1.0,
399 _name = "product.product"
400 _description = "Product"
401 _table = "product_product"
402 _inherits = {'product.template': 'product_tmpl_id'}
404 'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
405 'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
406 'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
407 'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
408 'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
409 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
410 'code': fields.function(_product_code, method=True, type='char', string='Code'),
411 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
412 'default_code' : fields.char('Code', size=64),
413 'active': fields.boolean('Active'),
414 'variants': fields.char('Variants', size=64),
415 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
416 'ean13': fields.char('EAN13', size=13),
417 'packaging' : fields.one2many('product.packaging', 'product_id', 'Palettization', 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."),
418 'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
419 'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
422 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
423 if uom_id and uom_po_id:
424 uom_obj=self.pool.get('product.uom')
425 uom=uom_obj.browse(cursor,user,[uom_id])[0]
426 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
427 print uom.category_id.id , uom_po.category_id.id
428 if uom.category_id.id != uom_po.category_id.id:
429 return {'value': {'uom_po_id': uom_id}}
432 def _check_ean_key(self, cr, uid, ids):
433 for partner in self.browse(cr, uid, ids):
434 if not partner.ean13:
436 if len(partner.ean13) < 12:
445 sum += int(partner.ean13[i])
447 sum += 3 * int(partner.ean13[i])
448 check = int(math.ceil(sum / 10.0) * 10 - sum)
449 if len(partner.ean13) == 12:
450 self.write(cr, uid, partner.id, {
451 'ean13': partner.ean13 + str(check)
453 elif check != int(partner.ean13[12]):
457 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
459 def on_order(self, cr, uid, ids, orderline, quantity):
462 def name_get(self, cr, user, ids, context={}):
466 #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
467 #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
468 name = d.get('name','')
469 code = d.get('default_code',False)
471 name = '[%s] %s' % (code,name)
473 name = name + ' - %s' % (d['variants'],)
474 return (d['id'], name)
475 result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
478 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
483 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
485 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
487 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
488 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
489 result = self.name_get(cr, user, ids, context)
493 # Could be overrided for variants matrices prices
495 def price_get(self, cr, uid, ids, ptype='list_price', context={}):
497 product_uom_obj = self.pool.get('product.uom')
499 for product in self.browse(cr, uid, ids, context=context):
500 res[product.id] = product[ptype] or 0.0
501 if ptype == 'list_price':
502 res[product.id] = (res[product.id] * product.price_margin) + \
505 uom = product.uos_id or product.uom_id
506 res[product.id] = product_uom_obj._compute_price(cr, uid,
507 uom.id, res[product.id], context['uom'])
510 def copy(self, cr, uid, id, default=None, context=None):
514 if ('variant' in context) and context['variant']:
515 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
516 'price_margin', 'price_extra']
517 data = self.read(cr, uid, id, fields=fields, context=context)
521 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
522 and data['product_tmpl_id'][0]
524 return self.create(cr, uid, data)
526 return super(product_product, self).copy(cr, uid, id, default=default,
530 class product_packaging(osv.osv):
531 _name = "product.packaging"
532 _description = "Conditionnement"
535 'name' : fields.char('Description', size=64),
536 'qty' : fields.float('Quantity by UL',
537 help="The total number of products you can put by UL."),
538 'ul' : fields.many2one('product.ul', 'Type of UL', required=True),
539 'ul_qty' : fields.integer('UL by layer'),
540 'rows' : fields.integer('Number of layer', required=True,
541 help='The number of layer on palette'),
542 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
543 'ean' : fields.char('EAN', size=14,
544 help="The EAN code of the transport unit."),
545 'code' : fields.char('Code', size=14,
546 help="The code of the transport unit."),
547 'weight': fields.float('Palette Weight',
548 help='The weight of the empty palette'),
549 'weight_ul': fields.float('UL Weight',
550 help='The weight of the empty UL'),
551 'height': fields.float('Height', help='The height of the palette'),
552 'width': fields.float('Width', help='The width of the palette'),
553 'length': fields.float('Length', help='The length of the palette'),
556 def _get_1st_ul(self, cr, uid, context={}):
557 cr.execute('select id from product_ul order by id asc limit 1')
559 return (res and res[0]) or False
562 'rows' : lambda *a : 3,
567 salt = '31' * 6 + '3'
569 for ean_part, salt_part in zip(ean, salt):
570 sum += int(ean_part) * int(salt_part)
571 return (10 - (sum % 10)) % 10
572 checksum = staticmethod(checksum)
577 class product_supplierinfo(osv.osv):
578 _name = "product.supplierinfo"
579 _description = "Information about a product supplier"
581 'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade'),
582 'product_name': fields.char('Partner product name', size=128),
583 'product_code': fields.char('Partner product reference', size=64),
584 'sequence' : fields.integer('Priority'),
585 'qty' : fields.float('Minimal quantity', required=True),
586 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
587 'delay' : fields.integer('Delivery delay', required=True),
588 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
591 'qty': lambda *a: 0.0,
592 'delay': lambda *a: 1,
595 product_supplierinfo()
598 class pricelist_partnerinfo(osv.osv):
599 _name = 'pricelist.partnerinfo'
601 'name': fields.char('Description', size=64),
602 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
603 'min_quantity': fields.float('Quantity', required=True),
604 'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
606 _order = 'min_quantity asc'
607 pricelist_partnerinfo()
611 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: