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 _
31 def ean_checksum(eancode):
32 """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
33 if len(eancode) <> 13:
39 reversevalue = eanvalue[::-1]
40 finalean=reversevalue[1:]
42 for i in range(len(finalean)):
44 oddsum += int(finalean[i])
46 evensum += int(finalean[i])
47 total=(oddsum * 3) + evensum
49 check = int(10 - math.ceil(total % 10.0)) %10
52 def check_ean(eancode):
53 """returns True if eancode is a valid ean13 string, or null"""
56 if len(eancode) <> 13:
62 return ean_checksum(eancode) == int(eancode[-1])
64 def sanitize_ean13(ean13):
65 """Creates and returns a valid ean13 from an invalid one"""
67 return "0000000000000"
68 ean13 = re.sub("[A-Za-z]","0",ean13);
69 ean13 = re.sub("[^0-9]","",ean13);
72 ean13 = ean13 + '0' * (13-len(ean13))
73 return ean13[:-1] + str(ean_checksum(ean13))
75 #----------------------------------------------------------
77 #----------------------------------------------------------
79 class product_uom_categ(osv.osv):
80 _name = 'product.uom.categ'
81 _description = 'Product uom categ'
83 'name': fields.char('Name', size=64, required=True, translate=True),
87 class product_uom(osv.osv):
89 _description = 'Product Unit of Measure'
91 def _compute_factor_inv(self, factor):
92 return factor and (1.0 / factor) or 0.0
94 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
96 for uom in self.browse(cursor, user, ids, context=context):
97 res[uom.id] = self._compute_factor_inv(uom.factor)
100 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
101 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
103 def create(self, cr, uid, data, context=None):
104 if 'factor_inv' in data:
105 if data['factor_inv'] <> 1:
106 data['factor'] = self._compute_factor_inv(data['factor_inv'])
107 del(data['factor_inv'])
108 return super(product_uom, self).create(cr, uid, data, context)
112 'name': fields.char('Name', size=64, required=True, translate=True),
113 'category_id': fields.many2one('product.uom.categ', 'Unit of Measure Category', required=True, ondelete='cascade',
114 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
115 'factor': fields.float('Ratio', required=True,digits=(12, 12),
116 help='How many times this Unit of Measure is smaller than the reference Unit of Measure in this category:\n'\
117 '1 * (reference unit) = ratio * (this unit)'),
118 'factor_inv': fields.function(_factor_inv, digits=(12,12),
119 fnct_inv=_factor_inv_write,
121 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
122 '1 * (this unit) = ratio * (reference unit)', required=True),
123 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
124 help="The computed quantity will be a multiple of this value. "\
125 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
126 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
127 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
128 ('reference','Reference Unit of Measure for this category'),
129 ('smaller','Smaller than the reference Unit of Measure')],'Unit of Measure Type', required=1),
135 'uom_type': 'reference',
139 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
142 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
143 if not from_uom_id or not qty or not to_uom_id:
145 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
146 if uoms[0].id == from_uom_id:
147 from_unit, to_unit = uoms[0], uoms[-1]
149 from_unit, to_unit = uoms[-1], uoms[0]
150 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
152 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
155 if from_unit.category_id.id <> to_unit.category_id.id:
156 if context.get('raise-exception', True):
157 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,))
160 amount = qty / from_unit.factor
162 amount = rounding(amount * to_unit.factor, to_unit.rounding)
165 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
166 if not from_uom_id or not price or not to_uom_id:
168 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
169 if uoms[0].id == from_uom_id:
170 from_unit, to_unit = uoms[0], uoms[-1]
172 from_unit, to_unit = uoms[-1], uoms[0]
173 if from_unit.category_id.id <> to_unit.category_id.id:
175 amount = price * from_unit.factor
177 amount = amount / to_unit.factor
180 def onchange_type(self, cursor, user, ids, value):
181 if value == 'reference':
182 return {'value': {'factor': 1, 'factor_inv': 1}}
185 def write(self, cr, uid, ids, vals, context=None):
186 if 'category_id' in vals:
187 for uom in self.browse(cr, uid, ids, context=context):
188 if uom.category_id.id != vals['category_id']:
189 raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
190 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
195 class product_ul(osv.osv):
197 _description = "Shipping Unit"
199 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
200 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
205 #----------------------------------------------------------
207 #----------------------------------------------------------
208 class product_category(osv.osv):
210 def name_get(self, cr, uid, ids, context=None):
211 if isinstance(ids, (list, tuple)) and not len(ids):
213 if isinstance(ids, (long, int)):
215 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
218 name = record['name']
219 if record['parent_id']:
220 name = record['parent_id'][1]+' / '+name
221 res.append((record['id'], name))
224 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
225 res = self.name_get(cr, uid, ids, context=context)
228 _name = "product.category"
229 _description = "Product Category"
231 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
232 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
233 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
234 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
235 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
236 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
237 'parent_left': fields.integer('Left Parent', select=1),
238 'parent_right': fields.integer('Right Parent', select=1),
243 'type' : lambda *a : 'normal',
246 _parent_name = "parent_id"
248 _parent_order = 'sequence, name'
249 _order = 'parent_left'
251 def _check_recursion(self, cr, uid, ids, context=None):
254 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
255 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
262 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
264 def child_get(self, cr, uid, ids):
270 #----------------------------------------------------------
272 #----------------------------------------------------------
273 class product_template(osv.osv):
274 _name = "product.template"
275 _description = "Product Template"
278 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
279 'product_manager': fields.many2one('res.users','Product Manager',help="Responsible for product."),
280 'description': fields.text('Description',translate=True),
281 'description_purchase': fields.text('Purchase Description',translate=True),
282 'description_sale': fields.text('Sale Description',translate=True),
283 '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."),
284 '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."),
285 '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."),
286 '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."),
287 '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."),
288 'rental': fields.boolean('Can be Rent'),
289 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
290 'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price for computing the customer price. Sometimes called the catalog price."),
291 'standard_price': fields.float('Cost', digits_compute=dp.get_precision('Product Price'), help="Product's cost for accounting stock valuation. It is the base price for the supplier price.", groups="base.group_user"),
292 'volume': fields.float('Volume', help="The volume in m3."),
293 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
294 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
295 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
296 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."),
297 'warranty': fields.float('Warranty'),
298 '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."),
299 '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."),
300 'state': fields.selection([('',''),
301 ('draft', 'In Development'),
302 ('sellable','Normal'),
303 ('end','End of Lifecycle'),
304 ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
305 'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
306 '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."),
307 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
308 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.'),
309 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
310 help='Coefficient to convert Unit of Measure to UOS\n'
311 ' uos = uom * coeff'),
312 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
313 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
314 'loc_rack': fields.char('Rack', size=16),
315 'loc_row': fields.char('Row', size=16),
316 'loc_case': fields.char('Case', size=16),
317 'company_id': fields.many2one('res.company', 'Company', select=1),
320 def _get_uom_id(self, cr, uid, *args):
321 cr.execute('select id from product_uom order by id limit 1')
323 return res and res[0] or False
325 def _default_category(self, cr, uid, context=None):
328 if 'categ_id' in context and context['categ_id']:
329 return context['categ_id']
330 md = self.pool.get('ir.model.data')
333 res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
338 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
340 return {'value': {'uom_po_id': uom_id}}
343 def write(self, cr, uid, ids, vals, context=None):
344 if 'uom_po_id' in vals:
345 new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
346 for product in self.browse(cr, uid, ids, context=context):
347 old_uom = product.uom_po_id
348 if old_uom.category_id.id != new_uom.category_id.id:
349 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 deactivate this product from the 'Procurements' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
350 return super(product_template, self).write(cr, uid, ids, vals, context=context)
353 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
354 'list_price': lambda *a: 1,
355 'cost_method': lambda *a: 'standard',
356 'supply_method': lambda *a: 'buy',
357 'standard_price': lambda *a: 0.0,
358 'sale_ok': lambda *a: 1,
359 'sale_delay': lambda *a: 7,
360 'produce_delay': lambda *a: 1,
361 'purchase_ok': lambda *a: 1,
362 'procure_method': lambda *a: 'make_to_stock',
363 'uom_id': _get_uom_id,
364 'uom_po_id': _get_uom_id,
365 'uos_coeff' : lambda *a: 1.0,
366 'mes_type' : lambda *a: 'fixed',
367 'categ_id' : _default_category,
368 'type' : lambda *a: 'consu',
371 def _check_uom(self, cursor, user, ids, context=None):
372 for product in self.browse(cursor, user, ids, context=context):
373 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
377 def _check_uos(self, cursor, user, ids, context=None):
378 for product in self.browse(cursor, user, ids, context=context):
380 and product.uos_id.category_id.id \
381 == product.uom_id.category_id.id:
386 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
389 def name_get(self, cr, user, ids, context=None):
392 if 'partner_id' in context:
394 return super(product_template, self).name_get(cr, user, ids, context)
398 class product_product(osv.osv):
399 def view_header_get(self, cr, uid, view_id, view_type, context=None):
402 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
403 if (context.get('categ_id', False)):
404 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
407 def _product_price(self, cr, uid, ids, name, arg, context=None):
411 quantity = context.get('quantity') or 1.0
412 pricelist = context.get('pricelist', False)
413 partner = context.get('partner', False)
417 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
422 res.setdefault(id, 0.0)
425 def _get_product_available_func(states, what):
426 def _product_available(self, cr, uid, ids, name, arg, context=None):
427 return {}.fromkeys(ids, 0.0)
428 return _product_available
430 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
431 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
432 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
433 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
435 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
437 product_uom_obj = self.pool.get('product.uom')
439 res.setdefault(id, 0.0)
440 for product in self.browse(cr, uid, ids, context=context):
442 uom = product.uos_id or product.uom_id
443 res[product.id] = product_uom_obj._compute_price(cr, uid,
444 uom.id, product.list_price, context['uom'])
446 res[product.id] = product.list_price
447 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
450 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
451 for supinfo in product.seller_ids:
452 if supinfo.name.id == partner_id:
453 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
454 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
457 def _product_code(self, cr, uid, ids, name, arg, context=None):
461 for p in self.browse(cr, uid, ids, context=context):
462 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
465 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
469 for p in self.browse(cr, uid, ids, context=context):
470 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
471 if not data['variants']:
472 data['variants'] = p.variants
474 data['code'] = p.code
476 data['name'] = p.name
477 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
478 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
482 def _get_main_product_supplier(self, cr, uid, product, context=None):
483 """Determines the main (best) product supplier for ``product``,
484 returning the corresponding ``supplierinfo`` record, or False
485 if none were found. The default strategy is to select the
486 supplier with the highest priority (i.e. smallest sequence).
488 :param browse_record product: product to supply
489 :rtype: product.supplierinfo browse_record or False
491 sellers = [(seller_info.sequence, seller_info)
492 for seller_info in product.seller_ids or []
493 if seller_info and isinstance(seller_info.sequence, (int, long))]
494 return sellers and sellers[0][1] or False
496 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
498 for product in self.browse(cr, uid, ids, context=context):
499 main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
500 result[product.id] = {
501 'seller_info_id': main_supplier and main_supplier.id or False,
502 'seller_delay': main_supplier.delay if main_supplier else 1,
503 'seller_qty': main_supplier and main_supplier.qty or 0.0,
504 'seller_id': main_supplier and main_supplier.name.id or False
509 def _get_image(self, cr, uid, ids, name, args, context=None):
510 result = dict.fromkeys(ids, False)
511 for obj in self.browse(cr, uid, ids, context=context):
512 result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
515 def _set_image(self, cr, uid, id, name, value, args, context=None):
516 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
519 'active': lambda *a: 1,
520 'price_extra': lambda *a: 0.0,
521 'price_margin': lambda *a: 1.0,
525 _name = "product.product"
526 _description = "Product"
527 _table = "product_product"
528 _inherits = {'product.template': 'product_tmpl_id'}
529 _inherit = ['mail.thread']
530 _order = 'default_code,name_template'
532 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
533 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
534 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
535 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
536 'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Product Price')),
537 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
538 'code': fields.function(_product_code, type='char', string='Internal Reference'),
539 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
540 'default_code' : fields.char('Internal Reference', size=64, select=True),
541 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
542 'variants': fields.char('Variants', size=64),
543 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
544 'ean13': fields.char('EAN13 Barcode', size=13, help="The numbers encoded in EAN-13 bar codes are product identification numbers."),
545 '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."),
546 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price')),
547 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price')),
548 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
549 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True, select=True),
550 'color': fields.integer('Color Index'),
551 # image: all image fields are base64 encoded and PIL-supported
552 'image': fields.binary("Image",
553 help="This field holds the image used as image for the product, limited to 1024x1024px."),
554 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
555 string="Medium-sized image", type="binary", multi="_get_image",
557 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
559 help="Medium-sized image of the product. It is automatically "\
560 "resized as a 128x128px image, with aspect ratio preserved, "\
561 "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
562 'image_small': fields.function(_get_image, fnct_inv=_set_image,
563 string="Small-sized image", type="binary", multi="_get_image",
565 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
567 help="Small-sized image of the product. It is automatically "\
568 "resized as a 64x64px image, with aspect ratio preserved. "\
569 "Use this field anywhere a small image is required."),
570 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", multi="seller_info"),
571 '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."),
572 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
573 '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"),
576 def create(self, cr, uid, vals, context=None):
577 obj_id = super(product_product, self).create(cr, uid, vals, context=context)
578 self.create_send_note(cr, uid, [obj_id], context=context)
581 def create_send_note(self, cr, uid, ids, context=None):
582 return self.message_post(cr, uid, ids, body=_("Product has been <b>created</b>."), context=context)
584 def unlink(self, cr, uid, ids, context=None):
586 unlink_product_tmpl_ids = []
587 for product in self.browse(cr, uid, ids, context=context):
588 tmpl_id = product.product_tmpl_id.id
589 # Check if the product is last product of this template
590 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
591 if not other_product_ids:
592 unlink_product_tmpl_ids.append(tmpl_id)
593 unlink_ids.append(product.id)
594 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
595 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
597 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
598 if uom_id and uom_po_id:
599 uom_obj=self.pool.get('product.uom')
600 uom=uom_obj.browse(cursor,user,[uom_id])[0]
601 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
602 if uom.category_id.id != uom_po.category_id.id:
603 return {'value': {'uom_po_id': uom_id}}
606 def _check_ean_key(self, cr, uid, ids, context=None):
607 for product in self.read(cr, uid, ids, ['ean13'], context=context):
608 res = check_ean(product['ean13'])
612 _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
614 def on_order(self, cr, uid, ids, orderline, quantity):
617 def name_get(self, cr, user, ids, context=None):
620 if isinstance(ids, (int, long)):
625 name = d.get('name','')
626 code = d.get('default_code',False)
628 name = '[%s] %s' % (code,name)
629 if d.get('variants'):
630 name = name + ' - %s' % (d['variants'],)
631 return (d['id'], name)
633 partner_id = context.get('partner_id', False)
636 for product in self.browse(cr, user, ids, context=context):
637 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
642 'name': s.product_name or product.name,
643 'default_code': s.product_code or product.default_code,
644 'variants': product.variants
646 result.append(_name_get(mydict))
650 'name': product.name,
651 'default_code': product.default_code,
652 'variants': product.variants
654 result.append(_name_get(mydict))
657 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
661 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
663 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
665 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
666 # on a database with thousands of matching products, due to the huge merge+unique needed for the
667 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
668 # Performing a quick memory merge of ids in Python will give much better performance
670 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
672 # we may underrun the limit because of dupes in the results, that's fine
673 ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit-len(ids)), context=context))
676 ptrn = re.compile('(\[(.*?)\])')
677 res = ptrn.search(name)
679 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
681 ids = self.search(cr, user, args, limit=limit, context=context)
682 result = self.name_get(cr, user, ids, context=context)
686 # Could be overrided for variants matrices prices
688 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
692 if 'currency_id' in context:
693 pricetype_obj = self.pool.get('product.price.type')
694 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
695 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
698 product_uom_obj = self.pool.get('product.uom')
699 for product in self.browse(cr, uid, ids, context=context):
700 res[product.id] = product[ptype] or 0.0
701 if ptype == 'list_price':
702 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
705 uom = product.uom_id or product.uos_id
706 res[product.id] = product_uom_obj._compute_price(cr, uid,
707 uom.id, res[product.id], context['uom'])
708 # Convert from price_type currency to asked one
709 if 'currency_id' in context:
710 # Take the price_type currency from the product field
711 # This is right cause a field cannot be in more than one currency
712 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
713 context['currency_id'], res[product.id],context=context)
717 def copy(self, cr, uid, id, default=None, context=None):
724 # Craft our own `<name> (copy)` in en_US (self.copy_translation()
725 # will do the other languages).
726 context_wo_lang = context.copy()
727 context_wo_lang.pop('lang', None)
728 product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
729 default = default.copy()
730 default['name'] = product['name'] + ' (copy)'
732 if context.get('variant',False):
733 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
734 'price_margin', 'price_extra']
735 data = self.read(cr, uid, id, fields=fields, context=context)
739 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
740 and data['product_tmpl_id'][0]
742 return self.create(cr, uid, data)
744 return super(product_product, self).copy(cr, uid, id, default=default,
748 class product_packaging(osv.osv):
749 _name = "product.packaging"
750 _description = "Packaging"
754 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
755 'name' : fields.text('Description', size=64),
756 'qty' : fields.float('Quantity by Package',
757 help="The total number of products you can put by pallet or box."),
758 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
759 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
760 'rows' : fields.integer('Number of Layers', required=True,
761 help='The number of layers on a pallet or box'),
762 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
763 'ean' : fields.char('EAN', size=14,
764 help="The EAN code of the package unit."),
765 'code' : fields.char('Code', size=14,
766 help="The code of the transport unit."),
767 'weight': fields.float('Total Package Weight',
768 help='The weight of a full package, pallet or box.'),
769 'weight_ul': fields.float('Empty Package Weight',
770 help='The weight of the empty UL'),
771 'height': fields.float('Height', help='The height of the package'),
772 'width': fields.float('Width', help='The width of the package'),
773 'length': fields.float('Length', help='The length of the package'),
777 def _check_ean_key(self, cr, uid, ids, context=None):
778 for pack in self.browse(cr, uid, ids, context=context):
779 res = check_ean(pack.ean)
782 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
784 def name_get(self, cr, uid, ids, context=None):
788 for pckg in self.browse(cr, uid, ids, context=context):
789 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
790 p_name += pckg.ul.name
791 res.append((pckg.id,p_name))
794 def _get_1st_ul(self, cr, uid, context=None):
795 cr.execute('select id from product_ul order by id asc limit 1')
797 return (res and res[0]) or False
800 'rows' : lambda *a : 3,
801 'sequence' : lambda *a : 1,
806 salt = '31' * 6 + '3'
808 for ean_part, salt_part in zip(ean, salt):
809 sum += int(ean_part) * int(salt_part)
810 return (10 - (sum % 10)) % 10
811 checksum = staticmethod(checksum)
816 class product_supplierinfo(osv.osv):
817 _name = "product.supplierinfo"
818 _description = "Information about a product supplier"
819 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
821 product_uom_pool = self.pool.get('product.uom')
822 for supplier_info in self.browse(cr, uid, ids, context=context):
824 result[supplier_info.id] = {field:False}
825 qty = supplier_info.min_qty
826 result[supplier_info.id]['qty'] = qty
830 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
831 '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."),
832 '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."),
833 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
834 '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."),
835 '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."),
836 '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."),
837 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
838 '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."),
839 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
840 'company_id':fields.many2one('res.company','Company',select=1),
843 'qty': lambda *a: 0.0,
844 'sequence': lambda *a: 1,
845 'delay': lambda *a: 1,
846 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
848 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
850 Calculate price from supplier pricelist.
851 @param supplier_ids: Ids of res.partner object.
852 @param product_id: Id of product.
853 @param product_qty: specify quantity to purchase.
855 if type(supplier_ids) in (int,long,):
856 supplier_ids = [supplier_ids]
858 product_pool = self.pool.get('product.product')
859 partner_pool = self.pool.get('res.partner')
860 pricelist_pool = self.pool.get('product.pricelist')
861 currency_pool = self.pool.get('res.currency')
862 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
863 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
864 # Compute price from standard price of product
865 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
867 # Compute price from Purchase pricelist of supplier
868 pricelist_id = supplier.property_product_pricelist_purchase.id
870 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
871 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
873 # Compute price from supplier pricelist which are in Supplier Information
874 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
875 if supplier_info_ids:
876 cr.execute('SELECT * ' \
877 'FROM pricelist_partnerinfo ' \
878 'WHERE suppinfo_id IN %s' \
879 'AND min_quantity <= %s ' \
880 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
881 res2 = cr.dictfetchone()
883 price = res2['price']
884 res[supplier.id] = price
887 product_supplierinfo()
890 class pricelist_partnerinfo(osv.osv):
891 _name = 'pricelist.partnerinfo'
893 'name': fields.char('Description', size=64),
894 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
895 '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."),
896 'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product 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"),
898 _order = 'min_quantity asc'
899 pricelist_partnerinfo()
900 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: