############################################################################## # # Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved. # # $Id: product.py 1310 2005-09-08 20:40:15Z pinky $ # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## from osv import fields from osv import osv from osv import orm import math def rounding(f, r): if not r: return f return round(f / r) * r def is_pair(x): return not x%2 #---------------------------------------------------------- # UOM #---------------------------------------------------------- class product_uom_categ(osv.osv): _name = 'product.uom.categ' _description = 'Product uom categ' _columns = { 'name': fields.char('Name', size=64, required=True), } product_uom_categ() class product_uom(osv.osv): _name = 'product.uom' _description = 'Product Unit of Measure' _columns = { 'name': fields.char('Name', size=64, required=True), 'category_id': fields.many2one('product.uom.categ', 'UOM Category', required=True, ondelete='cascade'), 'factor': fields.float('Factor', required=True), 'rounding': fields.float('Rounding Precision', required=True), 'active': fields.boolean('Active'), } _defaults = { 'factor': lambda *a: 1.0, 'active': lambda *a: 1, 'rounding': lambda *a: 0.01, } def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False): if not to_uom_id: to_uom_id = 0 if not from_uom_id or not qty: return qty f = self.read(cr, uid, [from_uom_id, to_uom_id], ['factor','rounding']) if f[0]['id'] == from_uom_id: from_unit, to_unit = f[0], f[-1] else: from_unit, to_unit = f[-1], f[0] amount = qty * from_unit['factor'] if to_uom_id: amount = rounding(amount / to_unit['factor'], to_unit['rounding']) return amount def _compute_price(self, cr, uid, uom_id, qty, to_uom_id=False): if not uom_id or not qty: return qty f = self.read(cr, uid, [uom_id], ['factor','rounding'])[0] return qty * f['factor'] product_uom() #---------------------------------------------------------- # Categories #---------------------------------------------------------- class product_category(osv.osv): _name = "product.category" _description = "Product Category" _columns = { 'name': fields.char('Name', size=64, required=True), 'intrastat' : fields.char('Intrastat number', size=16), 'parent_id': fields.many2one('product.category','Parent Category'), 'child_id': fields.one2many('product.category', 'parent_id', string='Childs Categories') } def child_get(self, cr, uid, ids): return [ids] product_category() #---------------------------------------------------------- # Products #---------------------------------------------------------- class product_template(osv.osv): _name = "product.template" _description = "Product Template" _columns = { 'name': fields.char('Name', size=64, required=True, translate=True), 'product_manager': fields.many2one('res.users','Product Manager'), 'description': fields.text('Description', translate=True), 'description_purchase': fields.text('Purchase Description', translate=True), 'description_sale': fields.text('Sale Description', translate=True), 'name_ids': fields.one2many('product.template.name', 'template_id', 'Names for Partners'), 'type': fields.selection([('product','Stockable Product'),('service','Service')], 'Product Type', required=True), 'supply_method': fields.selection([('produce','Produce'),('buy','Buy')], 'Supply method', required=True), 'seller_id': fields.many2one('res.partner', 'Default Supplier'), 'seller_delay': fields.float('Supplier lead time'), 'sale_delay': fields.float('Procurement lead time'), 'seller_ids': fields.many2many('res.partner', 'product_product_supplier', 'product_id', 'partner_id','Alternative Suppliers'), 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procure Method', required=True), 'rental': fields.boolean('Rentable product'), 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True), 'list_price': fields.float('List Price'), 'volume': fields.float('Volume'), 'weight': fields.float('Weight'), 'cost_method': fields.selection([('standard','Standard Price'), ('pmp','PMP (Not implemented!)'), ('fifo','FIFO')], 'Costing Method', required=True), 'standard_price': fields.float('Standard Price', required=True), 'limit_price': fields.float('Limit Price'), 'warranty': fields.float('Warranty (months)'), 'sale_ok': fields.boolean('Can be sold'), 'purchase_ok': fields.boolean('Can be Purchased'), 'taxes_id': fields.many2many('account.tax', 'product_taxes_rel', 'prod_id', 'tax_id', 'Product Taxes'), 'uom_id': fields.many2one('product.uom', 'Default UOM', required=True), 'uom_po_id': fields.many2one('product.uom', 'Purchase UOM', required=True), 'state': fields.selection([('draft', 'In Development'),('sellable','In production'),('end','End of lifecycle'),('obsolete','Obsolete')], 'State'), 'uos_id' : fields.many2one('product.uom', 'Secondary Unit', required=True), 'uos_coeff' : fields.float('UOM -> Sec. Coeff'), } def _get_uom_id(self, cr, uid, *args): cr.execute('select id from product_uom order by id limit 1') res = cr.fetchone() return res and res[0] or False _defaults = { 'type': lambda *a: 'product', 'list_price': lambda *a: 1, 'cost_method': lambda *a: 'standard', 'supply_method': lambda *a: 'buy', 'standard_price': lambda *a: 1, 'limit_price': lambda *a: 1, 'sale_ok': lambda *a: 1, 'sale_delay': lambda *a: 7, 'purchase_ok': lambda *a: 1, 'procure_method': lambda *a: 'make_to_stock', 'uom_id': _get_uom_id, 'uom_po_id': _get_uom_id, 'uom_price_id' : _get_uom_id, 'uos_id' : _get_uom_id, 'uos_coeff' : lambda *a: 1.0, #FIXME 'specific_bom': lambda *a: False, 'specific_routing': lambda *a: False, } # TODO: redefine name_get & name_search for product.template.name def name_get(self, cr, user, ids, context={}): if 'partner_id' in context: pass return orm.orm.name_get(self, cr, user, ids, context) product_template() class product_template_name(osv.osv): _name = "product.template.name" _description = "Product Template" _columns = { 'name': fields.char('Product Name', size=64, required=True), 'code': fields.char('Product Code', size=32), 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='cascade'), 'template_id': fields.many2one('product.template', 'Product Template', ondelete='cascade'), } product_template_name() class product_product(osv.osv): def _product_price(self, cr, uid, ids, name, arg, context={}): res = {} quantity = context.get('quantity', 1) pricelist = context.get('pricelist', False) if pricelist: for id in ids: price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, 'list', context=context)[pricelist] res[id] = price for id in ids: res.setdefault(id, 0.0) return res # # Utiliser browse pour limiter les queries ! # def _product_virtual_available(self, cr, uid, ids, name, arg, context={}): res = {} if ('shop' in context) and context['shop']: cr.execute('select warehouse_id from sale_shop where id=%d', (int(context['shop']),)) res = cr.fetchone() if res: context['warehouse'] = res[0] if context.get('warehouse',False): cr.execute('select lot_stock_id from stock_warehouse where id=%d', (int(context['warehouse']),)) res = cr.fetchone() context['location'] = res[0] if context.get('location',False): res = self.pool.get('stock.location')._product_virtual_get(cr, uid, context['location'], ids, context) for id in ids: res.setdefault(id, 0) return res def _product_qty_available(self, cr, uid, ids, name, arg, context={}): res = {} if 'shop' in context: cr.execute('select warehouse_id from sale_shop where id=%d', (int(context['shop']),)) res = cr.fetchone() or {} if res: context['warehouse'] = res[0] if context.get('warehouse',False): cr.execute('select lot_stock_id from stock_warehouse where id=%d', (int(context['warehouse']),)) res = cr.fetchone() or {} if res: context['location'] = res[0] if context.get('location',False): res = self.pool.get('stock.location')._product_all_get(cr, uid, context['location'], ids, context) or {} for id in ids: res.setdefault(id, 0) return res def _product_lst_price(self, cr, uid, ids, name, arg, context={}): res = {} for p in self.browse(cr, uid, ids): res[p.id] = p.list_price for id in ids: res.setdefault(id, 0) if 'uom' in context: for id in ids: res[id] = self.pool.get('product.uom')._compute_price(cr, uid, context['uom'], res[id]) return res def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id): product = self.browse(cr, uid, [product_id])[0] for name in product.name_ids: if name.partner_id.id == partner_id: return {'code' : name.code, 'name' : name.name} return {'code' : product.default_code, 'name' : product.name} def _product_code(self, cr, uid, ids, name, arg, context={}): res = {} for p in self.browse(cr, uid, ids): res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None))['code'] return res def _product_partner_ref(self, cr, uid, ids, name, arg, context={}): res = {} for p in self.browse(cr, uid, ids): res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None))['name'] return res _defaults = { 'active': lambda *a: 1, 'mes_type' : lambda *a: 'fixed', } _name = "product.product" _description = "Product" _table = "product_product" _inherits = {'product.template': 'product_tmpl_id'} _columns = { 'qty_available': fields.function(_product_qty_available, method=True, type='integer', string='Real Stock'), 'virtual_available': fields.function(_product_virtual_available, method=True, type='integer', string='Virtual Stock'), 'price': fields.function(_product_price, method=True, type='float', string='Customer Price'), 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List price'), 'code': fields.function(_product_code, method=True, type='char', string='Code'), 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'), 'default_code' : fields.char('Code', size=64), 'active': fields.boolean('Active'), 'variants': fields.char('Variants', size=64), 'list_price_margin': fields.float('List Price Margin'), 'list_price_extra': fields.float('List Price Extra'), 'standard_price_margin': fields.float('Standard Price Margin'), 'standard_price_extra': fields.float('Standard Price Extra'), 'limit_price_margin': fields.float('Limit Price Margin'), 'limit_price_extra': fields.float('Limit Price Extra'), 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True), 'ean13': fields.char('EAN13', size=13), 'packaging' : fields.one2many('product.packaging', 'product_id', 'Palettization'), 'mes_type' : fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Mesure type', required=True), 'tracking' : fields.boolean('Track lots'), } def _check_ean_key(self, cr, uid, ids): for partner_o in osv.osv_pools.get('product.product').read(cr, uid, ids, ['ean13',]): thisean=partner_o['ean13'] if thisean and thisean!='': if len(thisean)!=13: return False sum=0 for i in range(12): if is_pair(i): sum+=int(thisean[i]) else: sum+=3*int(thisean[i]) if math.ceil(sum/10.0)*10-sum!=int(thisean[12]): return False return True #_constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])] def on_order(self, cr, uid, ids, orderline, quantity): pass def name_get(self, cr, user, ids, context={}): if not len(ids): return [] def _name_get(d): name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']] code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']] if code: name = '[%s] %s' % (code,name) if d['variants']: name = name + ' - %s' % (d['variants'],) return (d['id'], name) result = map(_name_get, self.read(cr, user, ids, ['variants'])) return result def name_search(self, cr, user, name, args=[], operator='ilike', context={}): ids = self.search(cr, user, [('name',operator,name)]+ args) ids += self.search(cr, user, [('default_code','=',name)]+ args) return self.name_get(cr, user, ids) def price_get(self, cr, uid, ids, ptype='list', context={}): result = self.read(cr, uid, ids) result2 = {} for res in result: result2[res['id']] = (res[ptype+'_price']*(1.0+(res[ptype+'_price_margin'] or 0.0)))+(res[ptype+'_price_extra'] or 0.0) if 'uom' in context: result2[res['id']] = self.pool.get('product.uom')._compute_price(cr, uid, context['uom'], result2[res['id']]) return result2 product_product() #---------------------------------------------------------- # Price lists #---------------------------------------------------------- class product_pricelist(osv.osv): _name = "product.pricelist" _description = "Pricelist" _columns = { 'name': fields.char('Name',size=64, required=True), 'active': fields.boolean('Active'), 'version_id': fields.one2many('product.pricelist.version', 'pricelist_id', 'Pricelist Versions') } _defaults = { 'active': lambda *a: 1, } # # TODO: !!! if 'uom' in context: other price # def price_get(self, cr, uid, ids, prod_id, qty, type='list', context={}): result = {} # TODO FIXME for id in ids: cr.execute('select * from product_pricelist_version where pricelist_id=%d and active=True order by id limit 1', (id,)) plversion = cr.dictfetchone() if not plversion: raise 'pricelist', 'No active version for this pricelist !\nPlease create or active one.' cr.execute('select id,categ_id from product_template where id=(select product_tmpl_id from product_product where id=%d)', (prod_id,)) tmpl_id,categ = cr.fetchone() categ_ids = [] while categ: categ_ids.append(str(categ)) cr.execute('select parent_id from product_category where id=%d', (categ,)) categ = cr.fetchone()[0] if categ_ids: categ_where = '(categ_id in ('+','.join(categ_ids)+'))' else: categ_where = '(categ_id is null)' cr.execute('select * from product_pricelist_item where (product_tmpl_id is null or product_tmpl_id=%d) and ('+categ_where+' or (categ_id is null)) and price_version_id=%d and (min_quantity is null or min_quantity<=%d) order by priority limit 1', (tmpl_id, plversion['id'], qty)) res = cr.dictfetchone() if res: if res['base_pricelist_id'] and res['base']=='pricelist': price = self.price_get(cr, uid, [res['base_pricelist_id']], prod_id, qty, res[type+'_price_base'])[res['base_pricelist_id']] price_limit = self.price_get(cr, uid, [res['base_pricelist_id']], prod_id, qty, 'limit')[res['base_pricelist_id']] else: price = self.pool.get('product.product').price_get(cr, uid, [prod_id], res[type+'_price_base'])[prod_id] price_limit = self.pool.get('product.product').price_get(cr, uid, [prod_id], 'limit')[prod_id] price = price * (1.0-(res[type+'_price_discount'] or 0.0)) price=rounding(price, res[type+'_price_round']) price += (res[type+'_price_surcharge'] or 0.0) if res[type+'_price_min_margin']: price = max(price, price_limit+res[type+'_price_min_margin']) if res[type+'_price_max_margin']: price = min(price, price_limit+res[type+'_price_max_margin']) else: price=False result[id] = price if 'uom' in context: result[id] = self.pool.get('product.uom')._compute_price(cr, uid, context['uom'], result[id]) return result product_pricelist() class product_pricelist_version(osv.osv): _name = "product.pricelist.version" _description = "Pricelist Version" _columns = { 'pricelist_id': fields.many2one('product.pricelist', 'Price List', required=True), 'name': fields.char('Name', size=64, required=True), 'active': fields.boolean('Active'), 'items_id': fields.one2many('product.pricelist.item', 'price_version_id', 'Price List Items', required=True), 'date_start': fields.date('Start Date'), 'date_end': fields.date('End Date') } _defaults = { 'active': lambda *a: 1, } product_pricelist_version() class product_pricelist_item(osv.osv): _name = "product.pricelist.item" _description = "Pricelist item" _order = "priority" _defaults = { 'list_price_base': lambda *a: 'list', 'standard_price_base': lambda *a: 'standard', 'limit_price_base': lambda *a: 'limit', 'min_quantity': lambda *a: 1, 'priority': lambda *a: 5, } _columns = { 'name': fields.char('Name', size=64, required=True), 'price_version_id': fields.many2one('product.pricelist.version', 'Price List Version', required=True), 'product_tmpl_id': fields.many2one('product.template', 'Product Template'), 'categ_id': fields.many2one('product.category', 'Product Category'), 'min_quantity': fields.integer('Min. Quantity', required=True), 'priority': fields.integer('Priority', required=True), 'base': fields.selection((('product','Product'),('pricelist','Pricelist')), 'Based on', required=True), 'base_pricelist_id': fields.many2one('product.pricelist', 'Base Price List'), 'list_price_base': fields.selection((('list','List'),('standard','Standard'),('limit','Limit')),'List Price Base', required=True), 'list_price_surcharge': fields.float('List Price Surcharge'), 'list_price_discount': fields.float('List Price Discount'), 'list_price_round': fields.float('List Price Rounding'), 'list_price_min_margin': fields.float('List Price Min. Margin'), 'list_price_max_margin': fields.float('List Price Max. Margin'), 'standard_price_base': fields.selection((('list','List'),('standard','Standard'),('limit','Limit')),'Standard Price Base', required=True), 'standard_price_surcharge': fields.float('Standard Price Surcharge'), 'standard_price_discount': fields.float('Standard Price Discount'), 'standard_price_round': fields.float('Standard Price Rounding'), 'standard_price_min_margin': fields.float('Standard Price Min. Margin'), 'standard_price_max_margin': fields.float('Standard Price Max. Margin'), 'limit_price_base': fields.selection((('list','List'),('standard','Standard'),('limit','Limit')),'Limit Price Base', required=True), 'limit_price_surcharge': fields.float('Limit Price Surcharge'), 'limit_price_discount': fields.float('Limit Price Discount'), 'limit_price_round': fields.float('Limit Price Rounding'), 'limit_price_min_margin': fields.float('Limit Price Min. Margin'), 'limit_price_max_margin': fields.float('Limit Price Max. Margin'), } def product_id_change(self, cr, uid, ids, product_id, context={}): if not product_id: return {} prod = self.pool.get('product.product').read(cr, uid, [product_id], ['code','name']) if prod[0]['code']: return {'value': {'name': prod[0]['code']}} return {} product_pricelist_item() class product_ul(osv.osv): _name = "product.ul" _description = "Shipping Unit" _columns = { 'name' : fields.char('Name', size=64), 'type' : fields.selection([('box', 'Box'), ('carton', 'Carton')], 'Type', required=True), } product_ul() class product_packaging(osv.osv): _name = "product.packaging" _description = "Conditionnement" _columns = { 'name' : fields.char('Code', size=1, required=True), 'descr' : fields.char('Description', size=64), 'qty' : fields.float('Quantity by UL'), 'ul' : fields.many2one('product.ul', 'Type of UL', required=True), 'ul_qty' : fields.integer('UL by row'), 'rows' : fields.integer('# of rows', required=True), 'product_id' : fields.many2one('product.product', 'Product'), 'ean' : fields.char('EAN', size=14), 'weight': fields.float('Weight Palette'), 'weight_ul': fields.float('Weight UL'), 'active' : fields.boolean('Active'), } def _get_1st_ul(self, cr, uid, context={}): cr.execute('select id from product_ul order by id asc limit 1') res = cr.fetchone() return (res and res[0]) or False _defaults = { 'rows' : lambda *a : 3, 'ul' : _get_1st_ul, 'active' : lambda *a: 1, } def checksum(ean): salt = '31' * 6 + '3' sum = 0 for ean_part, salt_part in zip(ean, salt): sum += int(ean_part) * int(salt_part) return (10 - (sum % 10)) % 10 checksum = staticmethod(checksum) def on_change_product_id(self, cr, uid, ids, prod_ean, name): if prod_ean: ean = name + prod_ean[:-1] return {'value' : {'ean' : ean + str(self.checksum(ean))}} else: return {} product_packaging() # vim:noexpandtab: