1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from osv import osv, fields
23 import decimal_precision as dp
26 from _common import rounding
28 from tools.translate import _
33 def check_ean(eancode):
36 if len(eancode) <> 13:
46 reversevalue = eanvalue[::-1]
47 finalean=reversevalue[1:]
49 for i in range(len(finalean)):
51 oddsum += int(finalean[i])
53 evensum += int(finalean[i])
54 total=(oddsum * 3) + evensum
56 check = int(10 - math.ceil(total % 10.0)) %10
58 if check != int(eancode[-1]):
61 #----------------------------------------------------------
63 #----------------------------------------------------------
65 class product_uom_categ(osv.osv):
66 _name = 'product.uom.categ'
67 _description = 'Product uom categ'
69 'name': fields.char('Name', size=64, required=True, translate=True),
73 class product_uom(osv.osv):
75 _description = 'Product Unit of Measure'
77 def _compute_factor_inv(self, factor):
78 return factor and round(1.0 / factor, 6) or 0.0
80 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
82 for uom in self.browse(cursor, user, ids, context=context):
83 res[uom.id] = self._compute_factor_inv(uom.factor)
86 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
87 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
89 def create(self, cr, uid, data, context=None):
90 if 'factor_inv' in data:
91 if data['factor_inv'] <> 1:
92 data['factor'] = self._compute_factor_inv(data['factor_inv'])
93 del(data['factor_inv'])
94 return super(product_uom, self).create(cr, uid, data, context)
97 'name': fields.char('Name', size=64, required=True, translate=True),
98 'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
99 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
100 'factor': fields.float('Ratio', required=True,digits=(12, 12),
101 help='How many times this UoM is smaller than the reference UoM in this category:\n'\
102 '1 * (reference unit) = ratio * (this unit)'),
103 'factor_inv': fields.function(_factor_inv, digits_compute=dp.get_precision('Product UoM'),
104 fnct_inv=_factor_inv_write,
106 help='How many times this UoM is bigger than the reference UoM in this category:\n'\
107 '1 * (this unit) = ratio * (reference unit)', required=True),
108 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product UoM'), required=True,
109 help="The computed quantity will be a multiple of this value. "\
110 "Use 1.0 for a UoM that cannot be further split, such as a piece."),
111 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
112 'uom_type': fields.selection([('bigger','Bigger than the reference UoM'),
113 ('reference','Reference UoM for this category'),
114 ('smaller','Smaller than the reference UoM')],'UoM Type', required=1),
120 'uom_type': 'reference',
124 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
125 ('factor_category_id_uniq', 'unique (category_id, factor)', 'You can not have more than one UOM with same factor for same UOM category'),
128 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
129 if not from_uom_id or not qty 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 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
138 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
141 if from_unit.category_id.id <> to_unit.category_id.id:
142 if context.get('raise-exception', True):
143 raise osv.except_osv(_('Error !'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
146 amount = qty / from_unit.factor
148 amount = rounding(amount * to_unit.factor, to_unit.rounding)
151 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
152 if not from_uom_id or not price or not to_uom_id:
154 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
155 if uoms[0].id == from_uom_id:
156 from_unit, to_unit = uoms[0], uoms[-1]
158 from_unit, to_unit = uoms[-1], uoms[0]
159 if from_unit.category_id.id <> to_unit.category_id.id:
161 amount = price * from_unit.factor
163 amount = amount / to_unit.factor
166 def onchange_type(self, cursor, user, ids, value):
167 if value == 'reference':
168 return {'value': {'factor': 1, 'factor_inv': 1}}
174 class product_ul(osv.osv):
176 _description = "Shipping Unit"
178 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
179 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
184 #----------------------------------------------------------
186 #----------------------------------------------------------
187 class product_category(osv.osv):
189 def name_get(self, cr, uid, ids, context=None):
192 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
195 name = record['name']
196 if record['parent_id']:
197 name = record['parent_id'][1]+' / '+name
198 res.append((record['id'], name))
201 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
202 res = self.name_get(cr, uid, ids, context=context)
205 _name = "product.category"
206 _description = "Product Category"
208 'name': fields.char('Name', size=64, required=True, translate=True),
209 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
210 'parent_id': fields.many2one('product.category','Parent Category', select=True),
211 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
212 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
213 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
218 'type' : lambda *a : 'normal',
221 _order = "sequence, name"
222 def _check_recursion(self, cr, uid, ids, context=None):
225 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
226 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
233 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
235 def child_get(self, cr, uid, ids):
241 #----------------------------------------------------------
243 #----------------------------------------------------------
244 class product_template(osv.osv):
245 _name = "product.template"
246 _description = "Product Template"
247 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
249 for product in self.browse(cr, uid, ids, context=context):
251 result[product.id] = {field:False}
252 result[product.id]['seller_delay'] = 1
253 if product.seller_ids:
254 partner_list = sorted([(partner_id.sequence, partner_id)
255 for partner_id in product.seller_ids
256 if partner_id and isinstance(partner_id.sequence, (int, long))])
257 main_supplier = partner_list and partner_list[0] and partner_list[0][1] or False
258 result[product.id]['seller_delay'] = main_supplier and main_supplier.delay or 1
259 result[product.id]['seller_qty'] = main_supplier and main_supplier.qty or 0.0
260 result[product.id]['seller_id'] = main_supplier and main_supplier.name.id or False
264 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
265 'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
266 'description': fields.text('Description',translate=True),
267 'description_purchase': fields.text('Purchase Description',translate=True),
268 'description_sale': fields.text('Sale Description',translate=True),
269 'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumable are product where you don't manage stock."),
270 '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. Buy will trigger purchase orders when requested."),
271 'sale_delay': fields.float('Customer Lead Time', help="This is the average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
272 'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days 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 lead times will be summed for all levels and purchase orders."),
273 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
274 'rental': fields.boolean('Can be Rent'),
275 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
276 'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Sale Price'), help="Base price for computing the customer price. Sometimes called the catalog price."),
277 'standard_price': fields.float('Cost Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="Product's cost for accounting stock valuation. It is the base price for the supplier price."),
278 'volume': fields.float('Volume', help="The volume in m3."),
279 'weight': fields.float('Gross weight', help="The gross weight in Kg."),
280 'weight_net': fields.float('Net weight', help="The net weight in Kg."),
281 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
282 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."),
283 'warranty': fields.float('Warranty (months)'),
284 'sale_ok': fields.boolean('Can be Sold', help="Determines if the product can be visible in the list of product within a selection from a sale order line."),
285 '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."),
286 'state': fields.selection([('',''),
287 ('draft', 'In Development'),
288 ('sellable','Normal'),
289 ('end','End of Lifecycle'),
290 ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
291 'uom_id': fields.many2one('product.uom', 'Default Unit Of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
292 'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
293 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
294 help='Used by companies that manage two units of measure: invoicing and inventory management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
295 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
296 help='Coefficient to convert UOM to UOS\n'
297 ' uos = uom * coeff'),
298 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
299 'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_delay", 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."),
300 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_qty", help="This is minimum quantity to purchase from Main Supplier."),
301 'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_id"),
302 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
303 'loc_rack': fields.char('Rack', size=16),
304 'loc_row': fields.char('Row', size=16),
305 'loc_case': fields.char('Case', size=16),
306 'company_id': fields.many2one('res.company', 'Company',select=1),
309 def _get_uom_id(self, cr, uid, *args):
310 cr.execute('select id from product_uom order by id limit 1')
312 return res and res[0] or False
314 def _default_category(self, cr, uid, context=None):
317 if 'categ_id' in context and context['categ_id']:
318 return context['categ_id']
319 md = self.pool.get('ir.model.data')
322 res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
327 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
329 return {'value': {'uom_po_id': uom_id}}
333 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
334 'list_price': lambda *a: 1,
335 'cost_method': lambda *a: 'standard',
336 'supply_method': lambda *a: 'buy',
337 'standard_price': lambda *a: 1,
338 'sale_ok': lambda *a: 1,
339 'sale_delay': lambda *a: 7,
340 'produce_delay': lambda *a: 1,
341 'purchase_ok': lambda *a: 1,
342 'procure_method': lambda *a: 'make_to_stock',
343 'uom_id': _get_uom_id,
344 'uom_po_id': _get_uom_id,
345 'uos_coeff' : lambda *a: 1.0,
346 'mes_type' : lambda *a: 'fixed',
347 'categ_id' : _default_category,
348 'type' : lambda *a: 'consu',
351 def _check_uom(self, cursor, user, ids, context=None):
352 for product in self.browse(cursor, user, ids, context=context):
353 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
357 def _check_uos(self, cursor, user, ids, context=None):
358 for product in self.browse(cursor, user, ids, context=context):
360 and product.uos_id.category_id.id \
361 == product.uom_id.category_id.id:
366 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
369 def name_get(self, cr, user, ids, context=None):
372 if 'partner_id' in context:
374 return super(product_template, self).name_get(cr, user, ids, context)
378 class product_product(osv.osv):
379 def view_header_get(self, cr, uid, view_id, view_type, context=None):
382 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
383 if (context.get('categ_id', False)):
384 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
387 def _product_price(self, cr, uid, ids, name, arg, context=None):
391 quantity = context.get('quantity') or 1.0
392 pricelist = context.get('pricelist', False)
396 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
401 res.setdefault(id, 0.0)
404 def _get_product_available_func(states, what):
405 def _product_available(self, cr, uid, ids, name, arg, context=None):
406 return {}.fromkeys(ids, 0.0)
407 return _product_available
409 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
410 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
411 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
412 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
414 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
416 product_uom_obj = self.pool.get('product.uom')
418 res.setdefault(id, 0.0)
419 for product in self.browse(cr, uid, ids, context=context):
421 uom = product.uos_id or product.uom_id
422 res[product.id] = product_uom_obj._compute_price(cr, uid,
423 uom.id, product.list_price, context['uom'])
425 res[product.id] = product.list_price
426 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
429 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
430 for supinfo in product.seller_ids:
431 if supinfo.name.id == partner_id:
432 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
433 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
436 def _product_code(self, cr, uid, ids, name, arg, context=None):
440 for p in self.browse(cr, uid, ids, context=context):
441 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
444 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
448 for p in self.browse(cr, uid, ids, context=context):
449 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
450 if not data['variants']:
451 data['variants'] = p.variants
453 data['code'] = p.code
455 data['name'] = p.name
456 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
457 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
461 'active': lambda *a: 1,
462 'price_extra': lambda *a: 0.0,
463 'price_margin': lambda *a: 1.0,
466 _name = "product.product"
467 _description = "Product"
468 _table = "product_product"
469 _inherits = {'product.template': 'product_tmpl_id'}
470 _order = 'default_code,name_template'
472 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
473 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
474 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
475 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
476 'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
477 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
478 'code': fields.function(_product_code, type='char', string='Reference'),
479 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
480 'default_code' : fields.char('Reference', size=64),
481 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
482 'variants': fields.char('Variants', size=64),
483 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
484 'ean13': fields.char('EAN13', size=13),
485 '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 picking order and is mainly used if you use the EDI module."),
486 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
487 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
488 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
489 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True),
492 def unlink(self, cr, uid, ids, context=None):
494 unlink_product_tmpl_ids = []
495 for product in self.browse(cr, uid, ids, context=context):
496 tmpl_id = product.product_tmpl_id.id
497 # Check if the product is last product of this template
498 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
499 if not other_product_ids:
500 unlink_product_tmpl_ids.append(tmpl_id)
501 unlink_ids.append(product.id)
502 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
503 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
505 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
506 if uom_id and uom_po_id:
507 uom_obj=self.pool.get('product.uom')
508 uom=uom_obj.browse(cursor,user,[uom_id])[0]
509 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
510 if uom.category_id.id != uom_po.category_id.id:
511 return {'value': {'uom_po_id': uom_id}}
514 def _check_ean_key(self, cr, uid, ids, context=None):
515 for product in self.browse(cr, uid, ids, context=context):
516 res = check_ean(product.ean13)
519 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
521 def on_order(self, cr, uid, ids, orderline, quantity):
524 def name_get(self, cr, user, ids, context=None):
530 name = d.get('name','')
531 code = d.get('default_code',False)
533 name = '[%s] %s' % (code,name)
534 if d.get('variants'):
535 name = name + ' - %s' % (d['variants'],)
536 return (d['id'], name)
538 partner_id = context.get('partner_id', False)
541 for product in self.browse(cr, user, ids, context=context):
542 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
547 'name': s.product_name or product.name,
548 'default_code': s.product_code or product.default_code,
549 'variants': product.variants
551 result.append(_name_get(mydict))
555 'name': product.name,
556 'default_code': product.default_code,
557 'variants': product.variants
559 result.append(_name_get(mydict))
562 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
566 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
568 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
570 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
571 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
573 ptrn=re.compile('(\[(.*?)\])')
574 res = ptrn.search(name)
576 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
578 ids = self.search(cr, user, args, limit=limit, context=context)
579 result = self.name_get(cr, user, ids, context=context)
583 # Could be overrided for variants matrices prices
585 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
589 if 'currency_id' in context:
590 pricetype_obj = self.pool.get('product.price.type')
591 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
592 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
595 product_uom_obj = self.pool.get('product.uom')
596 for product in self.browse(cr, uid, ids, context=context):
597 res[product.id] = product[ptype] or 0.0
598 if ptype == 'list_price':
599 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
602 uom = product.uom_id or product.uos_id
603 res[product.id] = product_uom_obj._compute_price(cr, uid,
604 uom.id, res[product.id], context['uom'])
605 # Convert from price_type currency to asked one
606 if 'currency_id' in context:
607 # Take the price_type currency from the product field
608 # This is right cause a field cannot be in more than one currency
609 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
610 context['currency_id'], res[product.id],context=context)
614 def copy(self, cr, uid, id, default=None, context=None):
618 product = self.read(cr, uid, id, ['name'], context=context)
621 default = default.copy()
622 default['name'] = product['name'] + _(' (copy)')
624 if context.get('variant',False):
625 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
626 'price_margin', 'price_extra']
627 data = self.read(cr, uid, id, fields=fields, context=context)
631 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
632 and data['product_tmpl_id'][0]
634 return self.create(cr, uid, data)
636 return super(product_product, self).copy(cr, uid, id, default=default,
640 class product_packaging(osv.osv):
641 _name = "product.packaging"
642 _description = "Packaging"
646 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
647 'name' : fields.text('Description', size=64),
648 'qty' : fields.float('Quantity by Package',
649 help="The total number of products you can put by pallet or box."),
650 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
651 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
652 'rows' : fields.integer('Number of Layers', required=True,
653 help='The number of layers on a pallet or box'),
654 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
655 'ean' : fields.char('EAN', size=14,
656 help="The EAN code of the package unit."),
657 'code' : fields.char('Code', size=14,
658 help="The code of the transport unit."),
659 'weight': fields.float('Total Package Weight',
660 help='The weight of a full package, pallet or box.'),
661 'weight_ul': fields.float('Empty Package Weight',
662 help='The weight of the empty UL'),
663 'height': fields.float('Height', help='The height of the package'),
664 'width': fields.float('Width', help='The width of the package'),
665 'length': fields.float('Length', help='The length of the package'),
669 def _check_ean_key(self, cr, uid, ids, context=None):
670 for pack in self.browse(cr, uid, ids, context=context):
671 res = check_ean(pack.ean)
674 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
676 def name_get(self, cr, uid, ids, context=None):
680 for pckg in self.browse(cr, uid, ids, context=context):
681 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
682 p_name += pckg.ul.name
683 res.append((pckg.id,p_name))
686 def _get_1st_ul(self, cr, uid, context=None):
687 cr.execute('select id from product_ul order by id asc limit 1')
689 return (res and res[0]) or False
692 'rows' : lambda *a : 3,
693 'sequence' : lambda *a : 1,
698 salt = '31' * 6 + '3'
700 for ean_part, salt_part in zip(ean, salt):
701 sum += int(ean_part) * int(salt_part)
702 return (10 - (sum % 10)) % 10
703 checksum = staticmethod(checksum)
708 class product_supplierinfo(osv.osv):
709 _name = "product.supplierinfo"
710 _description = "Information about a product supplier"
711 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
713 product_uom_pool = self.pool.get('product.uom')
714 for supplier_info in self.browse(cr, uid, ids, context=context):
716 result[supplier_info.id] = {field:False}
717 if supplier_info.product_uom.id:
718 qty = product_uom_pool._compute_qty(cr, uid, supplier_info.product_uom.id, supplier_info.min_qty, to_uom_id=supplier_info.product_id.uom_id.id)
720 qty = supplier_info.min_qty
721 result[supplier_info.id]['qty'] = qty
724 def _get_uom_id(self, cr, uid, context=None):
727 return context.get('uom_id', False)
730 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
731 'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
732 'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
733 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
734 'product_uom': fields.many2one('product.uom', string="Supplier UoM", help="Choose here the Unit of Measure in which the prices and quantities are expressed below."),
735 'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product UoM if not empty, in the default unit of measure of the product otherwise."),
736 'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Uom."),
737 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
738 'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time 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."),
739 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
740 'company_id':fields.many2one('res.company','Company',select=1),
743 'qty': lambda *a: 0.0,
744 'sequence': lambda *a: 1,
745 'delay': lambda *a: 1,
746 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
747 'product_uom': _get_uom_id,
749 def _check_uom(self, cr, uid, ids, context=None):
750 for supplier_info in self.browse(cr, uid, ids, context=context):
751 if supplier_info.product_uom and supplier_info.product_uom.category_id.id <> supplier_info.product_id.uom_id.category_id.id:
756 (_check_uom, 'Error: The default UOM and the Supplier Product UOM must be in the same category.', ['product_uom']),
758 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
760 Calculate price from supplier pricelist.
761 @param supplier_ids: Ids of res.partner object.
762 @param product_id: Id of product.
763 @param product_qty: specify quantity to purchase.
765 if type(supplier_ids) in (int,long,):
766 supplier_ids = [supplier_ids]
768 product_pool = self.pool.get('product.product')
769 partner_pool = self.pool.get('res.partner')
770 pricelist_pool = self.pool.get('product.pricelist')
771 currency_pool = self.pool.get('res.currency')
772 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
773 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
774 # Compute price from standard price of product
775 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
777 # Compute price from Purchase pricelist of supplier
778 pricelist_id = supplier.property_product_pricelist_purchase.id
780 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
781 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
783 # Compute price from supplier pricelist which are in Supplier Information
784 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
785 if supplier_info_ids:
786 cr.execute('SELECT * ' \
787 'FROM pricelist_partnerinfo ' \
788 'WHERE suppinfo_id IN %s' \
789 'AND min_quantity <= %s ' \
790 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
791 res2 = cr.dictfetchone()
793 price = res2['price']
794 res[supplier.id] = price
797 product_supplierinfo()
800 class pricelist_partnerinfo(osv.osv):
801 _name = 'pricelist.partnerinfo'
803 'name': fields.char('Description', size=64),
804 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
805 'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier UoM if any or in the default UoM of the product otherrwise."),
806 'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="This price will be considered as a price for the supplier UoM if any or the default Unit of Measure of the product otherwise"),
808 _order = 'min_quantity asc'
809 pricelist_partnerinfo()
810 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: