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
29 from tools.translate import _
34 def check_ean(eancode):
37 if len(eancode) <> 13:
47 reversevalue = eanvalue[::-1]
48 finalean=reversevalue[1:]
50 for i in range(len(finalean)):
52 oddsum += int(finalean[i])
54 evensum += int(finalean[i])
55 total=(oddsum * 3) + evensum
57 check = int(10 - math.ceil(total % 10.0)) %10
59 if check != int(eancode[-1]):
62 #----------------------------------------------------------
64 #----------------------------------------------------------
66 class product_uom_categ(osv.osv):
67 _name = 'product.uom.categ'
68 _description = 'Product uom categ'
70 'name': fields.char('Name', size=64, required=True, translate=True),
74 class product_uom(osv.osv):
76 _description = 'Product Unit of Measure'
78 def _compute_factor_inv(self, factor):
79 return factor and (1.0 / factor) or 0.0
81 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
83 for uom in self.browse(cursor, user, ids, context=context):
84 res[uom.id] = self._compute_factor_inv(uom.factor)
87 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
88 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
90 def create(self, cr, uid, data, context=None):
91 if 'factor_inv' in data:
92 if data['factor_inv'] <> 1:
93 data['factor'] = self._compute_factor_inv(data['factor_inv'])
94 del(data['factor_inv'])
95 return super(product_uom, self).create(cr, uid, data, context)
99 'name': fields.char('Name', size=64, required=True, translate=True),
100 'category_id': fields.many2one('product.uom.categ', 'Unit of Measure Category', required=True, ondelete='cascade',
101 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
102 'factor': fields.float('Ratio', required=True,digits=(12, 12),
103 help='How many times this Unit of Measure is smaller than the reference Unit of Measure in this category:\n'\
104 '1 * (reference unit) = ratio * (this unit)'),
105 'factor_inv': fields.function(_factor_inv, digits=(12,12),
106 fnct_inv=_factor_inv_write,
108 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
109 '1 * (this unit) = ratio * (reference unit)', required=True),
110 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
111 help="The computed quantity will be a multiple of this value. "\
112 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
113 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
114 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
115 ('reference','Reference Unit of Measure for this category'),
116 ('smaller','Smaller than the reference Unit of Measure')],'Unit of Measure Type', required=1),
122 'uom_type': 'reference',
126 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
129 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
130 if not from_uom_id or not qty or not to_uom_id:
132 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
133 if uoms[0].id == from_uom_id:
134 from_unit, to_unit = uoms[0], uoms[-1]
136 from_unit, to_unit = uoms[-1], uoms[0]
137 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
139 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
142 if from_unit.category_id.id <> to_unit.category_id.id:
143 if context.get('raise-exception', True):
144 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,))
147 amount = qty / from_unit.factor
149 amount = rounding(amount * to_unit.factor, to_unit.rounding)
152 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
153 if not from_uom_id or not price or not to_uom_id:
155 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
156 if uoms[0].id == from_uom_id:
157 from_unit, to_unit = uoms[0], uoms[-1]
159 from_unit, to_unit = uoms[-1], uoms[0]
160 if from_unit.category_id.id <> to_unit.category_id.id:
162 amount = price * from_unit.factor
164 amount = amount / to_unit.factor
167 def onchange_type(self, cursor, user, ids, value):
168 if value == 'reference':
169 return {'value': {'factor': 1, 'factor_inv': 1}}
172 def write(self, cr, uid, ids, vals, context=None):
173 if 'category_id' in vals:
174 for uom in self.browse(cr, uid, ids, context=context):
175 if uom.category_id.id != vals['category_id']:
176 raise osv.except_osv(_('Warning'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
177 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
182 class product_ul(osv.osv):
184 _description = "Shipping Unit"
186 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
187 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
192 #----------------------------------------------------------
194 #----------------------------------------------------------
195 class product_category(osv.osv):
197 def name_get(self, cr, uid, ids, context=None):
200 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
203 name = record['name']
204 if record['parent_id']:
205 name = record['parent_id'][1]+' / '+name
206 res.append((record['id'], name))
209 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
210 res = self.name_get(cr, uid, ids, context=context)
213 _name = "product.category"
214 _description = "Product Category"
216 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
217 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
218 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
219 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
220 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
221 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
222 'parent_left': fields.integer('Left Parent', select=1),
223 'parent_right': fields.integer('Right Parent', select=1),
228 'type' : lambda *a : 'normal',
231 _parent_name = "parent_id"
233 _parent_order = 'sequence, name'
234 _order = 'parent_left'
236 def _check_recursion(self, cr, uid, ids, context=None):
239 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
240 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
247 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
249 def child_get(self, cr, uid, ids):
255 #----------------------------------------------------------
257 #----------------------------------------------------------
258 class product_template(osv.osv):
259 _name = "product.template"
260 _description = "Product Template"
262 def _get_main_product_supplier(self, cr, uid, product, context=None):
263 """Determines the main (best) product supplier for ``product``,
264 returning the corresponding ``supplierinfo`` record, or False
265 if none were found. The default strategy is to select the
266 supplier with the highest priority (i.e. smallest sequence).
268 :param browse_record product: product to supply
269 :rtype: product.supplierinfo browse_record or False
271 sellers = [(seller_info.sequence, seller_info)
272 for seller_info in product.seller_ids or []
273 if seller_info and isinstance(seller_info.sequence, (int, long))]
274 return sellers and sellers[0][1] or False
276 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
278 for product in self.browse(cr, uid, ids, context=context):
279 main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
280 result[product.id] = {
281 'seller_info_id': main_supplier and main_supplier.id or False,
282 'seller_delay': main_supplier and main_supplier.delay or 1,
283 'seller_qty': main_supplier and main_supplier.qty or 0.0,
284 'seller_id': main_supplier and main_supplier.name.id or False
289 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
290 'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
291 'description': fields.text('Description',translate=True),
292 'description_purchase': fields.text('Purchase Description',translate=True),
293 'description_sale': fields.text('Sale Description',translate=True),
294 '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."),
295 'supply_method': fields.selection([('produce','Manufacture'),('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."),
296 '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."),
297 '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."),
298 '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."),
299 'rental': fields.boolean('Can be Rent'),
300 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
301 '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."),
302 '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."),
303 'volume': fields.float('Volume', help="The volume in m3."),
304 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
305 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
306 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
307 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."),
308 'warranty': fields.float('Warranty (months)'),
309 '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."),
310 '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."),
311 'state': fields.selection([('',''),
312 ('draft', 'In Development'),
313 ('sellable','Normal'),
314 ('end','End of Lifecycle'),
315 ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
316 'uom_id': fields.many2one('product.uom', 'Default Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
317 '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."),
318 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
319 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 Unit of Measure.'),
320 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
321 help='Coefficient to convert Unit of Measure to UOS\n'
322 ' uos = uom * coeff'),
323 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
324 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", multi="seller_info"),
325 'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", 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."),
326 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
327 '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_info"),
328 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
329 'loc_rack': fields.char('Rack', size=16),
330 'loc_row': fields.char('Row', size=16),
331 'loc_case': fields.char('Case', size=16),
332 'company_id': fields.many2one('res.company', 'Company', select=1),
335 def _get_uom_id(self, cr, uid, *args):
336 cr.execute('select id from product_uom order by id limit 1')
338 return res and res[0] or False
340 def _default_category(self, cr, uid, context=None):
343 if 'categ_id' in context and context['categ_id']:
344 return context['categ_id']
345 md = self.pool.get('ir.model.data')
348 res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
353 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
355 return {'value': {'uom_po_id': uom_id}}
358 def write(self, cr, uid, ids, vals, context=None):
359 if 'uom_po_id' in vals:
360 new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
361 for product in self.browse(cr, uid, ids, context=context):
362 old_uom = product.uom_po_id
363 if old_uom.category_id.id != new_uom.category_id.id:
364 raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may desactivate this product from the 'Procurement & Locations' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
365 return super(product_template, self).write(cr, uid, ids, vals, context=context)
368 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
369 'list_price': lambda *a: 1,
370 'cost_method': lambda *a: 'standard',
371 'supply_method': lambda *a: 'buy',
372 'standard_price': lambda *a: 1,
373 'sale_ok': lambda *a: 1,
374 'sale_delay': lambda *a: 7,
375 'produce_delay': lambda *a: 1,
376 'purchase_ok': lambda *a: 1,
377 'procure_method': lambda *a: 'make_to_stock',
378 'uom_id': _get_uom_id,
379 'uom_po_id': _get_uom_id,
380 'uos_coeff' : lambda *a: 1.0,
381 'mes_type' : lambda *a: 'fixed',
382 'categ_id' : _default_category,
383 'type' : lambda *a: 'consu',
386 def _check_uom(self, cursor, user, ids, context=None):
387 for product in self.browse(cursor, user, ids, context=context):
388 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
392 def _check_uos(self, cursor, user, ids, context=None):
393 for product in self.browse(cursor, user, ids, context=context):
395 and product.uos_id.category_id.id \
396 == product.uom_id.category_id.id:
401 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
404 def name_get(self, cr, user, ids, context=None):
407 if 'partner_id' in context:
409 return super(product_template, self).name_get(cr, user, ids, context)
413 class product_product(osv.osv):
414 def view_header_get(self, cr, uid, view_id, view_type, context=None):
417 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
418 if (context.get('categ_id', False)):
419 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
422 def _product_price(self, cr, uid, ids, name, arg, context=None):
426 quantity = context.get('quantity') or 1.0
427 pricelist = context.get('pricelist', False)
428 partner = context.get('partner', False)
432 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
437 res.setdefault(id, 0.0)
440 def _get_product_available_func(states, what):
441 def _product_available(self, cr, uid, ids, name, arg, context=None):
442 return {}.fromkeys(ids, 0.0)
443 return _product_available
445 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
446 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
447 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
448 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
450 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
452 product_uom_obj = self.pool.get('product.uom')
454 res.setdefault(id, 0.0)
455 for product in self.browse(cr, uid, ids, context=context):
457 uom = product.uos_id or product.uom_id
458 res[product.id] = product_uom_obj._compute_price(cr, uid,
459 uom.id, product.list_price, context['uom'])
461 res[product.id] = product.list_price
462 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
465 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
466 for supinfo in product.seller_ids:
467 if supinfo.name.id == partner_id:
468 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
469 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
472 def _product_code(self, cr, uid, ids, name, arg, context=None):
476 for p in self.browse(cr, uid, ids, context=context):
477 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
480 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
484 for p in self.browse(cr, uid, ids, context=context):
485 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
486 if not data['variants']:
487 data['variants'] = p.variants
489 data['code'] = p.code
491 data['name'] = p.name
492 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
493 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
496 def _get_image(self, cr, uid, ids, name, args, context=None):
497 result = dict.fromkeys(ids, False)
498 for obj in self.browse(cr, uid, ids, context=context):
499 resized_image_dict = tools.get_resized_images(obj.image)
501 'image_medium': resized_image_dict['image_medium'],
502 'image_small': resized_image_dict['image_small'],
506 def _set_image(self, cr, uid, id, name, value, args, context=None):
507 return self.write(cr, uid, [id], {'image': tools.resize_image_big(value)}, context=context)
509 def onchange_image(self, cr, uid, ids, value, context=None):
510 return {'value': tools.get_resized_images(value)}
513 'active': lambda *a: 1,
514 'price_extra': lambda *a: 0.0,
515 'price_margin': lambda *a: 1.0,
519 _name = "product.product"
520 _description = "Product"
521 _table = "product_product"
522 _inherits = {'product.template': 'product_tmpl_id'}
523 _inherit = ['mail.thread']
524 _order = 'default_code,name_template'
526 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
527 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
528 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
529 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
530 'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
531 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
532 'code': fields.function(_product_code, type='char', string='Reference'),
533 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
534 'default_code' : fields.char('Reference', size=64, select=True),
535 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
536 'variants': fields.char('Variants', size=64),
537 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
538 'ean13': fields.char('EAN13', size=13, help="The numbers encoded in EAN-13 bar codes are product identification numbers."),
539 '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."),
540 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
541 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
542 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
543 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True, select=True),
544 'color': fields.integer('Color Index'),
545 'image': fields.binary("Image",
546 help="This field holds the image used for the product. "\
547 "The image is base64 encoded, and PIL-supported. "\
548 "It is limited to a 12024x1024 px image."),
549 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
550 string="Medium-sized image", type="binary", multi="_get_image",
552 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
554 help="Medium-sized image of the product. It is automatically "\
555 "resized as a 180x180px image, with aspect ratio kept. "\
556 "Use this field in form views or some kanban views."),
557 'image_small': fields.function(_get_image, fnct_inv=_set_image,
558 string="Smal-sized image", type="binary", multi="_get_image",
560 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
562 help="Small-sized image of the product. It is automatically "\
563 "resized as a 50x50px image, with aspect ratio keps. "\
564 "Use this field anywhere a small image is required."),
567 def create(self, cr, uid, vals, context=None):
568 obj_id = super(product_product, self).create(cr, uid, vals, context=context)
569 self.create_send_note(cr, uid, [obj_id], context=context)
572 def create_send_note(self, cr, uid, ids, context=None):
573 return self.message_append_note(cr, uid, ids, body=_("Product has been <b>created</b>."), context=context)
575 def unlink(self, cr, uid, ids, context=None):
577 unlink_product_tmpl_ids = []
578 for product in self.browse(cr, uid, ids, context=context):
579 tmpl_id = product.product_tmpl_id.id
580 # Check if the product is last product of this template
581 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
582 if not other_product_ids:
583 unlink_product_tmpl_ids.append(tmpl_id)
584 unlink_ids.append(product.id)
585 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
586 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
588 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
589 if uom_id and uom_po_id:
590 uom_obj=self.pool.get('product.uom')
591 uom=uom_obj.browse(cursor,user,[uom_id])[0]
592 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
593 if uom.category_id.id != uom_po.category_id.id:
594 return {'value': {'uom_po_id': uom_id}}
597 def _check_ean_key(self, cr, uid, ids, context=None):
598 for product in self.browse(cr, uid, ids, context=context):
599 res = check_ean(product.ean13)
602 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
604 def on_order(self, cr, uid, ids, orderline, quantity):
607 def name_get(self, cr, user, ids, context=None):
613 name = d.get('name','')
614 code = d.get('default_code',False)
616 name = '[%s] %s' % (code,name)
617 if d.get('variants'):
618 name = name + ' - %s' % (d['variants'],)
619 return (d['id'], name)
621 partner_id = context.get('partner_id', False)
624 for product in self.browse(cr, user, ids, context=context):
625 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
630 'name': s.product_name or product.name,
631 'default_code': s.product_code or product.default_code,
632 'variants': product.variants
634 result.append(_name_get(mydict))
638 'name': product.name,
639 'default_code': product.default_code,
640 'variants': product.variants
642 result.append(_name_get(mydict))
645 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
649 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
651 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
653 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
654 # on a database with thousands of matching products, due to the huge merge+unique needed for the
655 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
656 # Performing a quick memory merge of ids in Python will give much better performance
658 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
660 # we may underrun the limit because of dupes in the results, that's fine
661 ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit-len(ids)), context=context))
664 ptrn = re.compile('(\[(.*?)\])')
665 res = ptrn.search(name)
667 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
669 ids = self.search(cr, user, args, limit=limit, context=context)
670 result = self.name_get(cr, user, ids, context=context)
674 # Could be overrided for variants matrices prices
676 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
680 if 'currency_id' in context:
681 pricetype_obj = self.pool.get('product.price.type')
682 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
683 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
686 product_uom_obj = self.pool.get('product.uom')
687 for product in self.browse(cr, uid, ids, context=context):
688 res[product.id] = product[ptype] or 0.0
689 if ptype == 'list_price':
690 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
693 uom = product.uom_id or product.uos_id
694 res[product.id] = product_uom_obj._compute_price(cr, uid,
695 uom.id, res[product.id], context['uom'])
696 # Convert from price_type currency to asked one
697 if 'currency_id' in context:
698 # Take the price_type currency from the product field
699 # This is right cause a field cannot be in more than one currency
700 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
701 context['currency_id'], res[product.id],context=context)
705 def copy(self, cr, uid, id, default=None, context=None):
712 # Craft our own `<name> (copy)` in en_US (self.copy_translation()
713 # will do the other languages).
714 context_wo_lang = context.copy()
715 context_wo_lang.pop('lang', None)
716 product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
717 default = default.copy()
718 default['name'] = product['name'] + ' (copy)'
720 if context.get('variant',False):
721 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
722 'price_margin', 'price_extra']
723 data = self.read(cr, uid, id, fields=fields, context=context)
727 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
728 and data['product_tmpl_id'][0]
730 return self.create(cr, uid, data)
732 return super(product_product, self).copy(cr, uid, id, default=default,
736 class product_packaging(osv.osv):
737 _name = "product.packaging"
738 _description = "Packaging"
742 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
743 'name' : fields.text('Description', size=64),
744 'qty' : fields.float('Quantity by Package',
745 help="The total number of products you can put by pallet or box."),
746 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
747 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
748 'rows' : fields.integer('Number of Layers', required=True,
749 help='The number of layers on a pallet or box'),
750 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
751 'ean' : fields.char('EAN', size=14,
752 help="The EAN code of the package unit."),
753 'code' : fields.char('Code', size=14,
754 help="The code of the transport unit."),
755 'weight': fields.float('Total Package Weight',
756 help='The weight of a full package, pallet or box.'),
757 'weight_ul': fields.float('Empty Package Weight',
758 help='The weight of the empty UL'),
759 'height': fields.float('Height', help='The height of the package'),
760 'width': fields.float('Width', help='The width of the package'),
761 'length': fields.float('Length', help='The length of the package'),
765 def _check_ean_key(self, cr, uid, ids, context=None):
766 for pack in self.browse(cr, uid, ids, context=context):
767 res = check_ean(pack.ean)
770 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
772 def name_get(self, cr, uid, ids, context=None):
776 for pckg in self.browse(cr, uid, ids, context=context):
777 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
778 p_name += pckg.ul.name
779 res.append((pckg.id,p_name))
782 def _get_1st_ul(self, cr, uid, context=None):
783 cr.execute('select id from product_ul order by id asc limit 1')
785 return (res and res[0]) or False
788 'rows' : lambda *a : 3,
789 'sequence' : lambda *a : 1,
794 salt = '31' * 6 + '3'
796 for ean_part, salt_part in zip(ean, salt):
797 sum += int(ean_part) * int(salt_part)
798 return (10 - (sum % 10)) % 10
799 checksum = staticmethod(checksum)
804 class product_supplierinfo(osv.osv):
805 _name = "product.supplierinfo"
806 _description = "Information about a product supplier"
807 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
809 product_uom_pool = self.pool.get('product.uom')
810 for supplier_info in self.browse(cr, uid, ids, context=context):
812 result[supplier_info.id] = {field:False}
813 qty = supplier_info.min_qty
814 result[supplier_info.id]['qty'] = qty
818 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
819 '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."),
820 '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."),
821 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
822 'product_uom': fields.related('product_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
823 'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product Unit of Measure if not empty, in the default unit of measure of the product otherwise."),
824 'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Unit of Measure."),
825 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
826 '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."),
827 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
828 'company_id':fields.many2one('res.company','Company',select=1),
831 'qty': lambda *a: 0.0,
832 'sequence': lambda *a: 1,
833 'delay': lambda *a: 1,
834 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
836 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
838 Calculate price from supplier pricelist.
839 @param supplier_ids: Ids of res.partner object.
840 @param product_id: Id of product.
841 @param product_qty: specify quantity to purchase.
843 if type(supplier_ids) in (int,long,):
844 supplier_ids = [supplier_ids]
846 product_pool = self.pool.get('product.product')
847 partner_pool = self.pool.get('res.partner')
848 pricelist_pool = self.pool.get('product.pricelist')
849 currency_pool = self.pool.get('res.currency')
850 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
851 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
852 # Compute price from standard price of product
853 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
855 # Compute price from Purchase pricelist of supplier
856 pricelist_id = supplier.property_product_pricelist_purchase.id
858 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
859 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
861 # Compute price from supplier pricelist which are in Supplier Information
862 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
863 if supplier_info_ids:
864 cr.execute('SELECT * ' \
865 'FROM pricelist_partnerinfo ' \
866 'WHERE suppinfo_id IN %s' \
867 'AND min_quantity <= %s ' \
868 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
869 res2 = cr.dictfetchone()
871 price = res2['price']
872 res[supplier.id] = price
875 product_supplierinfo()
878 class pricelist_partnerinfo(osv.osv):
879 _name = 'pricelist.partnerinfo'
881 'name': fields.char('Description', size=64),
882 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
883 'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier Unit of Measure if any or in the default Unit of Measure of the product otherrwise."),
884 '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 Unit of Measure if any or the default Unit of Measure of the product otherwise"),
886 _order = 'min_quantity asc'
887 pricelist_partnerinfo()
888 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: