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 ##############################################################################
25 from _common import ceiling
27 from openerp import tools
28 from openerp.osv import osv, fields
29 from openerp.tools.translate import _
31 import openerp.addons.decimal_precision as dp
33 def ean_checksum(eancode):
34 """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
35 if len(eancode) <> 13:
41 reversevalue = eanvalue[::-1]
42 finalean=reversevalue[1:]
44 for i in range(len(finalean)):
46 oddsum += int(finalean[i])
48 evensum += int(finalean[i])
49 total=(oddsum * 3) + evensum
51 check = int(10 - math.ceil(total % 10.0)) %10
54 def check_ean(eancode):
55 """returns True if eancode is a valid ean13 string, or null"""
58 if len(eancode) <> 13:
64 return ean_checksum(eancode) == int(eancode[-1])
66 def sanitize_ean13(ean13):
67 """Creates and returns a valid ean13 from an invalid one"""
69 return "0000000000000"
70 ean13 = re.sub("[A-Za-z]","0",ean13);
71 ean13 = re.sub("[^0-9]","",ean13);
74 ean13 = ean13 + '0' * (13-len(ean13))
75 return ean13[:-1] + str(ean_checksum(ean13))
77 #----------------------------------------------------------
79 #----------------------------------------------------------
81 class product_uom_categ(osv.osv):
82 _name = 'product.uom.categ'
83 _description = 'Product uom categ'
85 'name': fields.char('Name', size=64, required=True, translate=True),
89 class product_uom(osv.osv):
91 _description = 'Product Unit of Measure'
93 def _compute_factor_inv(self, factor):
94 return factor and (1.0 / factor) or 0.0
96 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
98 for uom in self.browse(cursor, user, ids, context=context):
99 res[uom.id] = self._compute_factor_inv(uom.factor)
102 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
103 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
105 def name_create(self, cr, uid, name, context=None):
106 """ The UoM category and factor are required, so we'll have to add temporary values
107 for imported UoMs """
108 uom_categ = self.pool.get('product.uom.categ')
109 # look for the category based on the english name, i.e. no context on purpose!
110 # TODO: should find a way to have it translated but not created until actually used
111 categ_misc = 'Unsorted/Imported Units'
112 categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
114 categ_id = categ_id[0]
116 categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
117 uom_id = self.create(cr, uid, {self._rec_name: name,
118 'category_id': categ_id,
120 return self.name_get(cr, uid, [uom_id], context=context)[0]
122 def create(self, cr, uid, data, context=None):
123 if 'factor_inv' in data:
124 if data['factor_inv'] <> 1:
125 data['factor'] = self._compute_factor_inv(data['factor_inv'])
126 del(data['factor_inv'])
127 return super(product_uom, self).create(cr, uid, data, context)
131 'name': fields.char('Unit of Measure', size=64, required=True, translate=True),
132 'category_id': fields.many2one('product.uom.categ', 'Category', required=True, ondelete='cascade',
133 help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
134 'factor': fields.float('Ratio', required=True,digits=(12, 12),
135 help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
136 '1 * (reference unit) = ratio * (this unit)'),
137 'factor_inv': fields.function(_factor_inv, digits=(12,12),
138 fnct_inv=_factor_inv_write,
140 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
141 '1 * (this unit) = ratio * (reference unit)', required=True),
142 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
143 help="The computed quantity will be a multiple of this value. "\
144 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
145 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
146 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
147 ('reference','Reference Unit of Measure for this category'),
148 ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
154 'uom_type': 'reference',
158 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
161 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
162 if not from_uom_id or not qty or not to_uom_id:
164 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
165 if uoms[0].id == from_uom_id:
166 from_unit, to_unit = uoms[0], uoms[-1]
168 from_unit, to_unit = uoms[-1], uoms[0]
169 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
171 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
174 if from_unit.category_id.id <> to_unit.category_id.id:
175 if context.get('raise-exception', True):
176 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,))
179 amount = qty / from_unit.factor
181 amount = ceiling(amount * to_unit.factor, to_unit.rounding)
184 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
185 if not from_uom_id or not price or not to_uom_id:
187 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
188 if uoms[0].id == from_uom_id:
189 from_unit, to_unit = uoms[0], uoms[-1]
191 from_unit, to_unit = uoms[-1], uoms[0]
192 if from_unit.category_id.id <> to_unit.category_id.id:
194 amount = price * from_unit.factor
196 amount = amount / to_unit.factor
199 def onchange_type(self, cursor, user, ids, value):
200 if value == 'reference':
201 return {'value': {'factor': 1, 'factor_inv': 1}}
204 def write(self, cr, uid, ids, vals, context=None):
205 if 'category_id' in vals:
206 for uom in self.browse(cr, uid, ids, context=context):
207 if uom.category_id.id != vals['category_id']:
208 raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
209 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
214 class product_ul(osv.osv):
216 _description = "Shipping Unit"
218 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
219 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
224 #----------------------------------------------------------
226 #----------------------------------------------------------
227 class product_category(osv.osv):
229 def name_get(self, cr, uid, ids, context=None):
230 if isinstance(ids, (list, tuple)) and not len(ids):
232 if isinstance(ids, (long, int)):
234 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
237 name = record['name']
238 if record['parent_id']:
239 name = record['parent_id'][1]+' / '+name
240 res.append((record['id'], name))
243 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
244 res = self.name_get(cr, uid, ids, context=context)
247 _name = "product.category"
248 _description = "Product Category"
250 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
251 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
252 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
253 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
254 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
255 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type', help="A category of the view type is a virtual category that can be used as the parent of another category to create a hierarchical structure."),
256 'parent_left': fields.integer('Left Parent', select=1),
257 'parent_right': fields.integer('Right Parent', select=1),
262 'type' : lambda *a : 'normal',
265 _parent_name = "parent_id"
267 _parent_order = 'sequence, name'
268 _order = 'parent_left'
270 def _check_recursion(self, cr, uid, ids, context=None):
273 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
274 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
281 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
283 def child_get(self, cr, uid, ids):
289 #----------------------------------------------------------
291 #----------------------------------------------------------
292 class product_template(osv.osv):
293 _name = "product.template"
294 _description = "Product Template"
297 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
298 'product_manager': fields.many2one('res.users','Product Manager'),
299 'description': fields.text('Description',translate=True),
300 'description_purchase': fields.text('Purchase Description',translate=True),
301 'description_sale': fields.text('Sale Description',translate=True),
302 'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Consumable are product where you don't manage stock, a service is a non-material product provided by a company or an individual."),
303 'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."),
304 'rental': fields.boolean('Can be Rent'),
305 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
306 'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."),
307 'standard_price': fields.float('Cost', digits_compute=dp.get_precision('Product Price'), help="Cost price of the product used for standard stock valuation in accounting and used as a base price on purchase orders.", groups="base.group_user"),
308 'volume': fields.float('Volume', help="The volume in m3."),
309 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
310 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
311 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
312 help="Standard Price: The cost price is manually updated at the end of a specific period (usually every year). \nAverage Price: The cost price is recomputed at each incoming shipment."),
313 'warranty': fields.float('Warranty'),
314 'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
315 'state': fields.selection([('',''),
316 ('draft', 'In Development'),
317 ('sellable','Normal'),
318 ('end','End of Lifecycle'),
319 ('obsolete','Obsolete')], 'Status'),
320 'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
321 '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."),
322 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
323 help='Sepcify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
324 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
325 help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
326 ' uos = uom * coeff'),
327 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
328 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Supplier'),
329 'company_id': fields.many2one('res.company', 'Company', select=1),
332 def _get_uom_id(self, cr, uid, *args):
333 cr.execute('select id from product_uom order by id limit 1')
335 return res and res[0] or False
337 def _default_category(self, cr, uid, context=None):
340 if 'categ_id' in context and context['categ_id']:
341 return context['categ_id']
342 md = self.pool.get('ir.model.data')
345 res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
350 def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
352 return {'value': {'uom_po_id': uom_id}}
355 def write(self, cr, uid, ids, vals, context=None):
356 if 'uom_po_id' in vals:
357 new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
358 for product in self.browse(cr, uid, ids, context=context):
359 old_uom = product.uom_po_id
360 if old_uom.category_id.id != new_uom.category_id.id:
361 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,))
362 return super(product_template, self).write(cr, uid, ids, vals, context=context)
365 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
367 'cost_method': 'standard',
368 'standard_price': 0.0,
371 'uom_id': _get_uom_id,
372 'uom_po_id': _get_uom_id,
374 'mes_type' : 'fixed',
375 'categ_id' : _default_category,
379 def _check_uom(self, cursor, user, ids, context=None):
380 for product in self.browse(cursor, user, ids, context=context):
381 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
385 def _check_uos(self, cursor, user, ids, context=None):
386 for product in self.browse(cursor, user, ids, context=context):
388 and product.uos_id.category_id.id \
389 == product.uom_id.category_id.id:
394 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
397 def name_get(self, cr, user, ids, context=None):
400 if 'partner_id' in context:
402 return super(product_template, self).name_get(cr, user, ids, context)
406 class product_product(osv.osv):
407 def view_header_get(self, cr, uid, view_id, view_type, context=None):
410 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
411 if (context.get('categ_id', False)):
412 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
415 def _product_price(self, cr, uid, ids, name, arg, context=None):
419 quantity = context.get('quantity') or 1.0
420 pricelist = context.get('pricelist', False)
421 partner = context.get('partner', False)
423 # Support context pricelists specified as display_name or ID for compatibility
424 if isinstance(pricelist, basestring):
425 pricelist_ids = self.pool.get('product.pricelist').name_search(
426 cr, uid, pricelist, operator='=', context=context, limit=1)
427 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
430 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
435 res.setdefault(id, 0.0)
438 def _get_product_available_func(states, what):
439 def _product_available(self, cr, uid, ids, name, arg, context=None):
440 return {}.fromkeys(ids, 0.0)
441 return _product_available
443 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
444 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
445 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
446 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
448 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
450 product_uom_obj = self.pool.get('product.uom')
452 res.setdefault(id, 0.0)
453 for product in self.browse(cr, uid, ids, context=context):
455 uom = product.uos_id or product.uom_id
456 res[product.id] = product_uom_obj._compute_price(cr, uid,
457 uom.id, product.list_price, context['uom'])
459 res[product.id] = product.list_price
460 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
463 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
464 for supinfo in product.seller_ids:
465 if supinfo.name.id == partner_id:
466 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
467 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
470 def _product_code(self, cr, uid, ids, name, arg, context=None):
474 for p in self.browse(cr, uid, ids, context=context):
475 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
478 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
482 for p in self.browse(cr, uid, ids, context=context):
483 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
484 if not data['variants']:
485 data['variants'] = p.variants
487 data['code'] = p.code
489 data['name'] = p.name
490 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
491 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
495 def _get_main_product_supplier(self, cr, uid, product, context=None):
496 """Determines the main (best) product supplier for ``product``,
497 returning the corresponding ``supplierinfo`` record, or False
498 if none were found. The default strategy is to select the
499 supplier with the highest priority (i.e. smallest sequence).
501 :param browse_record product: product to supply
502 :rtype: product.supplierinfo browse_record or False
504 sellers = [(seller_info.sequence, seller_info)
505 for seller_info in product.seller_ids or []
506 if seller_info and isinstance(seller_info.sequence, (int, long))]
507 return sellers and sellers[0][1] or False
509 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
511 for product in self.browse(cr, uid, ids, context=context):
512 main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
513 result[product.id] = {
514 'seller_info_id': main_supplier and main_supplier.id or False,
515 'seller_delay': main_supplier.delay if main_supplier else 1,
516 'seller_qty': main_supplier and main_supplier.qty or 0.0,
517 'seller_id': main_supplier and main_supplier.name.id or False
522 def _get_image(self, cr, uid, ids, name, args, context=None):
523 result = dict.fromkeys(ids, False)
524 for obj in self.browse(cr, uid, ids, context=context):
525 result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
528 def _set_image(self, cr, uid, id, name, value, args, context=None):
529 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
532 'active': lambda *a: 1,
533 'price_extra': lambda *a: 0.0,
534 'price_margin': lambda *a: 1.0,
538 _name = "product.product"
539 _description = "Product"
540 _table = "product_product"
541 _inherits = {'product.template': 'product_tmpl_id'}
542 _inherit = ['mail.thread']
543 _order = 'default_code,name_template'
545 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
546 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
547 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
548 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
549 'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
550 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
551 'code': fields.function(_product_code, type='char', string='Internal Reference'),
552 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
553 'default_code' : fields.char('Internal Reference', size=64, select=True),
554 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
555 'variants': fields.char('Variants', size=64),
556 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
557 'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
558 '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."),
559 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price')),
560 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price')),
561 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
562 'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', size=128, store=True, select=True),
563 'color': fields.integer('Color Index'),
564 # image: all image fields are base64 encoded and PIL-supported
565 'image': fields.binary("Image",
566 help="This field holds the image used as image for the product, limited to 1024x1024px."),
567 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
568 string="Medium-sized image", type="binary", multi="_get_image",
570 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
572 help="Medium-sized image of the product. It is automatically "\
573 "resized as a 128x128px image, with aspect ratio preserved, "\
574 "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
575 'image_small': fields.function(_get_image, fnct_inv=_set_image,
576 string="Small-sized image", type="binary", multi="_get_image",
578 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
580 help="Small-sized image of the product. It is automatically "\
581 "resized as a 64x64px image, with aspect ratio preserved. "\
582 "Use this field anywhere a small image is required."),
583 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
584 '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."),
585 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
586 '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"),
588 def unlink(self, cr, uid, ids, context=None):
590 unlink_product_tmpl_ids = []
591 for product in self.browse(cr, uid, ids, context=context):
592 tmpl_id = product.product_tmpl_id.id
593 # Check if the product is last product of this template
594 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
595 if not other_product_ids:
596 unlink_product_tmpl_ids.append(tmpl_id)
597 unlink_ids.append(product.id)
598 res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
599 # delete templates after calling super, as deleting template could lead to deleting
600 # products due to ondelete='cascade'
601 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
604 def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
605 if uom_id and uom_po_id:
606 uom_obj=self.pool.get('product.uom')
607 uom=uom_obj.browse(cursor,user,[uom_id])[0]
608 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
609 if uom.category_id.id != uom_po.category_id.id:
610 return {'value': {'uom_po_id': uom_id}}
613 def _check_ean_key(self, cr, uid, ids, context=None):
614 for product in self.read(cr, uid, ids, ['ean13'], context=context):
615 res = check_ean(product['ean13'])
619 _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
621 def on_order(self, cr, uid, ids, orderline, quantity):
624 def name_get(self, cr, user, ids, context=None):
627 if isinstance(ids, (int, long)):
632 name = d.get('name','')
633 code = d.get('default_code',False)
635 name = '[%s] %s' % (code,name)
636 if d.get('variants'):
637 name = name + ' - %s' % (d['variants'],)
638 return (d['id'], name)
640 partner_id = context.get('partner_id', False)
643 for product in self.browse(cr, user, ids, context=context):
644 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
649 'name': s.product_name or product.name,
650 'default_code': s.product_code or product.default_code,
651 'variants': product.variants
653 result.append(_name_get(mydict))
657 'name': product.name,
658 'default_code': product.default_code,
659 'variants': product.variants
661 result.append(_name_get(mydict))
664 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
668 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
670 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
672 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
673 # on a database with thousands of matching products, due to the huge merge+unique needed for the
674 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
675 # Performing a quick memory merge of ids in Python will give much better performance
677 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
678 if not limit or len(ids) < limit:
679 # we may underrun the limit because of dupes in the results, that's fine
680 ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit and (limit-len(ids)) or False) , context=context))
683 ptrn = re.compile('(\[(.*?)\])')
684 res = ptrn.search(name)
686 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
688 ids = self.search(cr, user, args, limit=limit, context=context)
689 result = self.name_get(cr, user, ids, context=context)
693 # Could be overrided for variants matrices prices
695 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
699 if 'currency_id' in context:
700 pricetype_obj = self.pool.get('product.price.type')
701 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
702 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
705 product_uom_obj = self.pool.get('product.uom')
706 for product in self.browse(cr, uid, ids, context=context):
707 res[product.id] = product[ptype] or 0.0
708 if ptype == 'list_price':
709 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
712 uom = product.uom_id or product.uos_id
713 res[product.id] = product_uom_obj._compute_price(cr, uid,
714 uom.id, res[product.id], context['uom'])
715 # Convert from price_type currency to asked one
716 if 'currency_id' in context:
717 # Take the price_type currency from the product field
718 # This is right cause a field cannot be in more than one currency
719 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
720 context['currency_id'], res[product.id],context=context)
724 def copy(self, cr, uid, id, default=None, context=None):
731 # Craft our own `<name> (copy)` in en_US (self.copy_translation()
732 # will do the other languages).
733 context_wo_lang = context.copy()
734 context_wo_lang.pop('lang', None)
735 product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
736 default = default.copy()
737 default.update(name=_("%s (copy)") % (product['name']))
739 if context.get('variant',False):
740 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
741 'price_margin', 'price_extra']
742 data = self.read(cr, uid, id, fields=fields, context=context)
746 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
747 and data['product_tmpl_id'][0]
749 return self.create(cr, uid, data)
751 return super(product_product, self).copy(cr, uid, id, default=default,
754 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
757 if context and context.get('search_default_categ_id', False):
758 args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
759 return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
763 class product_packaging(osv.osv):
764 _name = "product.packaging"
765 _description = "Packaging"
769 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
770 'name' : fields.text('Description', size=64),
771 'qty' : fields.float('Quantity by Package',
772 help="The total number of products you can put by pallet or box."),
773 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
774 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
775 'rows' : fields.integer('Number of Layers', required=True,
776 help='The number of layers on a pallet or box'),
777 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
778 'ean' : fields.char('EAN', size=14,
779 help="The EAN code of the package unit."),
780 'code' : fields.char('Code', size=14,
781 help="The code of the transport unit."),
782 'weight': fields.float('Total Package Weight',
783 help='The weight of a full package, pallet or box.'),
784 'weight_ul': fields.float('Empty Package Weight'),
785 'height': fields.float('Height', help='The height of the package'),
786 'width': fields.float('Width', help='The width of the package'),
787 'length': fields.float('Length', help='The length of the package'),
791 def _check_ean_key(self, cr, uid, ids, context=None):
792 for pack in self.browse(cr, uid, ids, context=context):
793 res = check_ean(pack.ean)
796 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
798 def name_get(self, cr, uid, ids, context=None):
802 for pckg in self.browse(cr, uid, ids, context=context):
803 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
804 p_name += pckg.ul.name
805 res.append((pckg.id,p_name))
808 def _get_1st_ul(self, cr, uid, context=None):
809 cr.execute('select id from product_ul order by id asc limit 1')
811 return (res and res[0]) or False
814 'rows' : lambda *a : 3,
815 'sequence' : lambda *a : 1,
820 salt = '31' * 6 + '3'
822 for ean_part, salt_part in zip(ean, salt):
823 sum += int(ean_part) * int(salt_part)
824 return (10 - (sum % 10)) % 10
825 checksum = staticmethod(checksum)
830 class product_supplierinfo(osv.osv):
831 _name = "product.supplierinfo"
832 _description = "Information about a product supplier"
833 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
835 product_uom_pool = self.pool.get('product.uom')
836 for supplier_info in self.browse(cr, uid, ids, context=context):
838 result[supplier_info.id] = {field:False}
839 qty = supplier_info.min_qty
840 result[supplier_info.id]['qty'] = qty
844 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
845 '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."),
846 '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."),
847 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
848 '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."),
849 '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."),
850 '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."),
851 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
852 '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."),
853 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
854 'company_id':fields.many2one('res.company','Company',select=1),
857 'qty': lambda *a: 0.0,
858 'sequence': lambda *a: 1,
859 'delay': lambda *a: 1,
860 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
862 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
864 Calculate price from supplier pricelist.
865 @param supplier_ids: Ids of res.partner object.
866 @param product_id: Id of product.
867 @param product_qty: specify quantity to purchase.
869 if type(supplier_ids) in (int,long,):
870 supplier_ids = [supplier_ids]
872 product_pool = self.pool.get('product.product')
873 partner_pool = self.pool.get('res.partner')
874 pricelist_pool = self.pool.get('product.pricelist')
875 currency_pool = self.pool.get('res.currency')
876 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
877 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
878 # Compute price from standard price of product
879 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
881 # Compute price from Purchase pricelist of supplier
882 pricelist_id = supplier.property_product_pricelist_purchase.id
884 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
885 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
887 # Compute price from supplier pricelist which are in Supplier Information
888 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
889 if supplier_info_ids:
890 cr.execute('SELECT * ' \
891 'FROM pricelist_partnerinfo ' \
892 'WHERE suppinfo_id IN %s' \
893 'AND min_quantity <= %s ' \
894 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
895 res2 = cr.dictfetchone()
897 price = res2['price']
898 res[supplier.id] = price
901 product_supplierinfo()
904 class pricelist_partnerinfo(osv.osv):
905 _name = 'pricelist.partnerinfo'
907 'name': fields.char('Description', size=64),
908 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
909 '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."),
910 '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"),
912 _order = 'min_quantity asc'
913 pricelist_partnerinfo()
915 class res_currency(osv.osv):
916 _inherit = 'res.currency'
918 def _check_main_currency_rounding(self, cr, uid, ids, context=None):
919 cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
920 digits = cr.fetchone()
921 if digits and len(digits):
923 main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
924 for currency_id in ids:
925 if currency_id == main_currency.id:
926 if main_currency.rounding < 10 ** -digits:
931 (_check_main_currency_rounding, 'Error! You cannot define a rounding factor for the company\'s main currency that is smaller than the decimal precision of \'Account\'.', ['rounding']),
934 class decimal_precision(osv.osv):
935 _inherit = 'decimal.precision'
937 def _check_main_currency_rounding(self, cr, uid, ids, context=None):
938 cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
941 account_precision_id, digits = res
942 main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
943 for decimal_precision in ids:
944 if decimal_precision == account_precision_id:
945 if main_currency.rounding < 10 ** -digits:
950 (_check_main_currency_rounding, 'Error! You cannot define the decimal precision of \'Account\' as greater than the rounding factor of the company\'s main currency', ['digits']),
953 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: