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 SUPERUSER_ID
28 from openerp import tools
29 from openerp.osv import osv, fields, expression
30 from openerp.tools.translate import _
31 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
34 import openerp.addons.decimal_precision as dp
35 from openerp.tools.float_utils import float_round, float_compare
37 def ean_checksum(eancode):
38 """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
39 if len(eancode) != 13:
45 reversevalue = eanvalue[::-1]
46 finalean=reversevalue[1:]
48 for i in range(len(finalean)):
50 oddsum += int(finalean[i])
52 evensum += int(finalean[i])
53 total=(oddsum * 3) + evensum
55 check = int(10 - math.ceil(total % 10.0)) %10
58 def check_ean(eancode):
59 """returns True if eancode is a valid ean13 string, or null"""
62 if len(eancode) != 13:
68 return ean_checksum(eancode) == int(eancode[-1])
70 def sanitize_ean13(ean13):
71 """Creates and returns a valid ean13 from an invalid one"""
73 return "0000000000000"
74 ean13 = re.sub("[A-Za-z]","0",ean13);
75 ean13 = re.sub("[^0-9]","",ean13);
78 ean13 = ean13 + '0' * (13-len(ean13))
79 return ean13[:-1] + str(ean_checksum(ean13))
81 #----------------------------------------------------------
83 #----------------------------------------------------------
85 class product_uom_categ(osv.osv):
86 _name = 'product.uom.categ'
87 _description = 'Product uom categ'
89 'name': fields.char('Name', required=True, translate=True),
92 class product_uom(osv.osv):
94 _description = 'Product Unit of Measure'
96 def _compute_factor_inv(self, factor):
97 return factor and (1.0 / factor) or 0.0
99 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
101 for uom in self.browse(cursor, user, ids, context=context):
102 res[uom.id] = self._compute_factor_inv(uom.factor)
105 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
106 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
108 def name_create(self, cr, uid, name, context=None):
109 """ The UoM category and factor are required, so we'll have to add temporary values
110 for imported UoMs """
111 uom_categ = self.pool.get('product.uom.categ')
112 # look for the category based on the english name, i.e. no context on purpose!
113 # TODO: should find a way to have it translated but not created until actually used
114 categ_misc = 'Unsorted/Imported Units'
115 categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
117 categ_id = categ_id[0]
119 categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
120 uom_id = self.create(cr, uid, {self._rec_name: name,
121 'category_id': categ_id,
123 return self.name_get(cr, uid, [uom_id], context=context)[0]
125 def create(self, cr, uid, data, context=None):
126 if 'factor_inv' in data:
127 if data['factor_inv'] != 1:
128 data['factor'] = self._compute_factor_inv(data['factor_inv'])
129 del(data['factor_inv'])
130 return super(product_uom, self).create(cr, uid, data, context)
134 'name': fields.char('Unit of Measure', required=True, translate=True),
135 'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade',
136 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."),
137 'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision
138 help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
139 '1 * (reference unit) = ratio * (this unit)'),
140 'factor_inv': fields.function(_factor_inv, digits=0, # force NUMERIC with unlimited precision
141 fnct_inv=_factor_inv_write,
142 string='Bigger Ratio',
143 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
144 '1 * (this unit) = ratio * (reference unit)', required=True),
145 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
146 help="The computed quantity will be a multiple of this value. "\
147 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
148 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
149 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
150 ('reference','Reference Unit of Measure for this category'),
151 ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
158 'uom_type': 'reference',
162 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
165 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True):
166 if not from_uom_id or not qty 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 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round)
175 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, context=None):
178 if from_unit.category_id.id != to_unit.category_id.id:
179 if context.get('raise-exception', True):
180 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,))
183 amount = qty/from_unit.factor
185 amount = amount * to_unit.factor
187 amount = ceiling(amount, to_unit.rounding)
190 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
191 if not from_uom_id or not price or not to_uom_id:
193 from_unit, to_unit = self.browse(cr, uid, [from_uom_id, to_uom_id])
194 if from_unit.category_id.id != to_unit.category_id.id:
196 amount = price * from_unit.factor
198 amount = amount / to_unit.factor
201 def onchange_type(self, cursor, user, ids, value):
202 if value == 'reference':
203 return {'value': {'factor': 1, 'factor_inv': 1}}
206 def write(self, cr, uid, ids, vals, context=None):
207 if isinstance(ids, (int, long)):
209 if 'category_id' in vals:
210 for uom in self.browse(cr, uid, ids, context=context):
211 if uom.category_id.id != vals['category_id']:
212 raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
213 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
217 class product_ul(osv.osv):
219 _description = "Logistic Unit"
221 'name' : fields.char('Name', select=True, required=True, translate=True),
222 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
223 'height': fields.float('Height', help='The height of the package'),
224 'width': fields.float('Width', help='The width of the package'),
225 'length': fields.float('Length', help='The length of the package'),
226 'weight': fields.float('Empty Package Weight'),
230 #----------------------------------------------------------
232 #----------------------------------------------------------
233 class product_category(osv.osv):
235 def name_get(self, cr, uid, ids, context=None):
236 if isinstance(ids, (list, tuple)) and not len(ids):
238 if isinstance(ids, (long, int)):
240 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
243 name = record['name']
244 if record['parent_id']:
245 name = record['parent_id'][1]+' / '+name
246 res.append((record['id'], name))
249 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
255 # Be sure name_search is symetric to name_get
256 name = name.split(' / ')[-1]
257 ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
259 ids = self.search(cr, uid, args, limit=limit, context=context)
260 return self.name_get(cr, uid, ids, context)
262 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
263 res = self.name_get(cr, uid, ids, context=context)
266 _name = "product.category"
267 _description = "Product Category"
269 'name': fields.char('Name', required=True, translate=True, select=True),
270 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
271 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
272 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
273 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
274 '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."),
275 'parent_left': fields.integer('Left Parent', select=1),
276 'parent_right': fields.integer('Right Parent', select=1),
284 _parent_name = "parent_id"
286 _parent_order = 'sequence, name'
287 _order = 'parent_left'
290 (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
294 class produce_price_history(osv.osv):
296 Keep track of the ``product.template`` standard prices as they are changed.
299 _name = 'product.price.history'
300 _rec_name = 'datetime'
301 _order = 'datetime desc'
304 'company_id': fields.many2one('res.company', required=True),
305 'product_template_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
306 'datetime': fields.datetime('Historization Time'),
307 'cost': fields.float('Historized Cost'),
310 def _get_default_company(self, cr, uid, context=None):
311 if 'force_company' in context:
312 return context['force_company']
314 company = self.pool['res.users'].browse(cr, uid, uid,
315 context=context).company_id
316 return company.id if company else False
319 'datetime': fields.datetime.now,
320 'company_id': _get_default_company,
324 #----------------------------------------------------------
326 #----------------------------------------------------------
327 class product_attribute(osv.osv):
328 _name = "product.attribute"
329 _description = "Product Attribute"
331 'name': fields.char('Name', translate=True, required=True),
332 'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values', copy=True),
335 class product_attribute_value(osv.osv):
336 _name = "product.attribute.value"
338 def _get_price_extra(self, cr, uid, ids, name, args, context=None):
339 result = dict.fromkeys(ids, 0)
340 if not context.get('active_id'):
343 for obj in self.browse(cr, uid, ids, context=context):
344 for price_id in obj.price_ids:
345 if price_id.product_tmpl_id.id == context.get('active_id'):
346 result[obj.id] = price_id.price_extra
350 def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
353 if 'active_id' not in context:
355 p_obj = self.pool['product.attribute.price']
356 p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
358 p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
360 p_obj.create(cr, uid, {
361 'product_tmpl_id': context['active_id'],
363 'price_extra': value,
366 def name_get(self, cr, uid, ids, context=None):
367 if context and not context.get('show_attribute', True):
368 return super(product_attribute_value, self).name_get(cr, uid, ids, context=context)
370 for value in self.browse(cr, uid, ids, context=context):
371 res.append([value.id, "%s: %s" % (value.attribute_id.name, value.name)])
375 'sequence': fields.integer('Sequence', help="Determine the display order"),
376 'name': fields.char('Value', translate=True, required=True),
377 'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='cascade'),
378 'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
379 'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
380 fnct_inv=_set_price_extra,
381 digits_compute=dp.get_precision('Product Price'),
382 help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
383 'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
386 ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
391 def unlink(self, cr, uid, ids, context=None):
392 ctx = dict(context or {}, active_test=False)
393 product_ids = self.pool['product.product'].search(cr, uid, [('attribute_value_ids', 'in', ids)], context=ctx)
395 raise osv.except_osv(_('Integrity Error!'), _('The operation cannot be completed:\nYou trying to delete an attribute value with a reference on a product variant.'))
396 return super(product_attribute_value, self).unlink(cr, uid, ids, context=context)
398 class product_attribute_price(osv.osv):
399 _name = "product.attribute.price"
401 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
402 'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True, ondelete='cascade'),
403 'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
406 class product_attribute_line(osv.osv):
407 _name = "product.attribute.line"
408 _rec_name = 'attribute_id'
410 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
411 'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='restrict'),
412 'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
416 #----------------------------------------------------------
418 #----------------------------------------------------------
419 class product_template(osv.osv):
420 _name = "product.template"
421 _inherit = ['mail.thread']
422 _description = "Product Template"
425 def _get_image(self, cr, uid, ids, name, args, context=None):
426 result = dict.fromkeys(ids, False)
427 for obj in self.browse(cr, uid, ids, context=context):
428 result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
431 def _set_image(self, cr, uid, id, name, value, args, context=None):
432 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
434 def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
435 return self._is_product_variant_impl(cr, uid, ids, name, arg, context=context)
437 def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
438 return dict.fromkeys(ids, False)
440 def _product_template_price(self, cr, uid, ids, name, arg, context=None):
441 plobj = self.pool.get('product.pricelist')
443 quantity = context.get('quantity') or 1.0
444 pricelist = context.get('pricelist', False)
445 partner = context.get('partner', False)
447 # Support context pricelists specified as display_name or ID for compatibility
448 if isinstance(pricelist, basestring):
449 pricelist_ids = plobj.name_search(
450 cr, uid, pricelist, operator='=', context=context, limit=1)
451 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
453 if isinstance(pricelist, (int, long)):
454 products = self.browse(cr, uid, ids, context=context)
455 qtys = map(lambda x: (x, quantity, partner), products)
456 pl = plobj.browse(cr, uid, pricelist, context=context)
457 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
459 res[id] = price.get(id, 0.0)
461 res.setdefault(id, 0.0)
464 def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
468 date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
469 price_history_obj = self.pool.get('product.price.history')
470 history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
472 return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
475 def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
476 ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
479 price_history_obj = self.pool['product.price.history']
480 user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
481 company_id = context.get('force_company', user_company)
482 price_history_obj.create(cr, uid, {
483 'product_template_id': product_tmpl_id,
485 'company_id': company_id,
488 def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
490 for product in self.browse(cr, uid, ids):
491 res[product.id] = len(product.product_variant_ids)
495 'name': fields.char('Name', required=True, translate=True, select=True),
496 'product_manager': fields.many2one('res.users','Product Manager'),
497 'description': fields.text('Description',translate=True,
498 help="A precise description of the Product, used only for internal information purposes."),
499 'description_purchase': fields.text('Purchase Description',translate=True,
500 help="A description of the Product that you want to communicate to your suppliers. "
501 "This description will be copied to every Purchase Order, Receipt and Supplier Invoice/Refund."),
502 'description_sale': fields.text('Sale Description',translate=True,
503 help="A description of the Product that you want to communicate to your customers. "
504 "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
505 '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."),
506 'rental': fields.boolean('Can be Rent'),
507 'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
508 'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
509 '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."),
510 'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
511 'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'),
512 help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.",
513 groups="base.group_user", string="Cost Price"),
514 'volume': fields.float('Volume', help="The volume in m3."),
515 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
516 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
517 'warranty': fields.float('Warranty'),
518 'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
519 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
520 'state': fields.selection([('draft', 'In Development'),
521 ('sellable','Normal'),
522 ('end','End of Lifecycle'),
523 ('obsolete','Obsolete')], 'Status'),
524 'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
525 '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."),
526 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
527 help='Specify 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.'),
528 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
529 help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
530 ' uos = uom * coeff'),
531 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
532 'company_id': fields.many2one('res.company', 'Company', select=1),
533 # image: all image fields are base64 encoded and PIL-supported
534 'image': fields.binary("Image",
535 help="This field holds the image used as image for the product, limited to 1024x1024px."),
536 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
537 string="Medium-sized image", type="binary", multi="_get_image",
539 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
541 help="Medium-sized image of the product. It is automatically "\
542 "resized as a 128x128px image, with aspect ratio preserved, "\
543 "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
544 'image_small': fields.function(_get_image, fnct_inv=_set_image,
545 string="Small-sized image", type="binary", multi="_get_image",
547 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
549 help="Small-sized image of the product. It is automatically "\
550 "resized as a 64x64px image, with aspect ratio preserved. "\
551 "Use this field anywhere a small image is required."),
552 'packaging_ids': fields.one2many(
553 'product.packaging', 'product_tmpl_id', 'Logistical Units',
554 help="Gives the different ways to package the same product. This has no impact on "
555 "the picking order and is mainly used if you use the EDI module."),
556 'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
557 'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
558 help="This is the average delay in days between the purchase order confirmation and the receipts for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
559 'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
560 help="This is minimum quantity to purchase from Main Supplier."),
561 'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
562 help="Main Supplier who has highest priority in Supplier List."),
564 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
565 'color': fields.integer('Color Index'),
566 'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Is product variant'),
568 'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
569 'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
570 'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
572 # related to display product product information if is_product_variant
573 'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
574 'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
577 def _price_get_list_price(self, product):
580 def _price_get(self, cr, uid, products, ptype='list_price', context=None):
584 if 'currency_id' in context:
585 pricetype_obj = self.pool.get('product.price.type')
586 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
587 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
590 product_uom_obj = self.pool.get('product.uom')
591 for product in products:
592 # standard_price field can only be seen by users in base.group_user
593 # Thus, in order to compute the sale price from the cost price for users not in this group
594 # We fetch the standard price as the superuser
595 if ptype != 'standard_price':
596 res[product.id] = product[ptype] or 0.0
598 company_id = product.env.user.company_id.id
599 product = product.with_context(force_company=company_id)
600 res[product.id] = res[product.id] = product.sudo()[ptype]
601 if ptype == 'list_price':
602 res[product.id] += product._name == "product.product" and product.price_extra or 0.0
604 uom = product.uom_id or product.uos_id
605 res[product.id] = product_uom_obj._compute_price(cr, uid,
606 uom.id, res[product.id], context['uom'])
607 # Convert from price_type currency to asked one
608 if 'currency_id' in context:
609 # Take the price_type currency from the product field
610 # This is right cause a field cannot be in more than one currency
611 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
612 context['currency_id'], res[product.id],context=context)
616 def _get_uom_id(self, cr, uid, *args):
617 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
619 def _default_category(self, cr, uid, context=None):
622 if 'categ_id' in context and context['categ_id']:
623 return context['categ_id']
624 md = self.pool.get('ir.model.data')
627 res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
632 def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
634 return {'value': {'uom_po_id': uom_id}}
637 def create_variant_ids(self, cr, uid, ids, context=None):
638 product_obj = self.pool.get("product.product")
639 ctx = context and context.copy() or {}
641 if ctx.get("create_product_variant"):
644 ctx.update(active_test=False, create_product_variant=True)
646 tmpl_ids = self.browse(cr, uid, ids, context=ctx)
647 for tmpl_id in tmpl_ids:
649 # list of values combination
652 for variant_id in tmpl_id.attribute_line_ids:
653 if len(variant_id.value_ids) == 1:
654 variant_alone.append(variant_id.value_ids[0])
656 for variant in all_variants:
657 for value_id in variant_id.value_ids:
658 temp_variants.append(variant + [int(value_id)])
659 all_variants = temp_variants
661 # adding an attribute with only one value should not recreate product
662 # write this attribute on every product to make sure we don't lose them
663 for variant_id in variant_alone:
665 for product_id in tmpl_id.product_variant_ids:
666 if variant_id.id not in map(int, product_id.attribute_value_ids):
667 product_ids.append(product_id.id)
668 product_obj.write(cr, uid, product_ids, {'attribute_value_ids': [(4, variant_id.id)]}, context=ctx)
671 variant_ids_to_active = []
672 variants_active_ids = []
673 variants_inactive = []
674 for product_id in tmpl_id.product_variant_ids:
675 variants = map(int,product_id.attribute_value_ids)
676 if variants in all_variants:
677 variants_active_ids.append(product_id.id)
678 all_variants.pop(all_variants.index(variants))
679 if not product_id.active:
680 variant_ids_to_active.append(product_id.id)
682 variants_inactive.append(product_id)
683 if variant_ids_to_active:
684 product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
687 for variant_ids in all_variants:
689 'product_tmpl_id': tmpl_id.id,
690 'attribute_value_ids': [(6, 0, variant_ids)]
692 id = product_obj.create(cr, uid, values, context=ctx)
693 variants_active_ids.append(id)
695 # unlink or inactive product
696 for variant_id in map(int,variants_inactive):
699 product_obj.unlink(cr, uid, [variant_id], context=ctx)
700 except (psycopg2.Error, osv.except_osv):
701 product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
705 def create(self, cr, uid, vals, context=None):
706 ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
707 product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
708 if not context or "create_product_product" not in context:
709 self.create_variant_ids(cr, uid, [product_template_id], context=context)
710 self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
712 # TODO: this is needed to set given values to first variant after creation
713 # these fields should be moved to product as lead to confusion
715 if vals.get('ean13'):
716 related_vals['ean13'] = vals['ean13']
717 if vals.get('default_code'):
718 related_vals['default_code'] = vals['default_code']
720 self.write(cr, uid, product_template_id, related_vals, context=context)
722 return product_template_id
724 def write(self, cr, uid, ids, vals, context=None):
725 ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
726 if isinstance(ids, (int, long)):
728 if 'standard_price' in vals:
729 for prod_template_id in ids:
730 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
731 res = super(product_template, self).write(cr, uid, ids, vals, context=context)
732 if 'attribute_line_ids' in vals or vals.get('active'):
733 self.create_variant_ids(cr, uid, ids, context=context)
734 if 'active' in vals and not vals.get('active'):
735 ctx = context and context.copy() or {}
736 ctx.update(active_test=False)
738 for product in self.browse(cr, uid, ids, context=ctx):
739 product_ids = map(int,product.product_variant_ids)
740 self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
743 def copy(self, cr, uid, id, default=None, context=None):
746 template = self.browse(cr, uid, id, context=context)
747 default['name'] = _("%s (copy)") % (template['name'])
748 return super(product_template, self).copy(cr, uid, id, default=default, context=context)
751 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
753 'standard_price': 0.0,
755 'uom_id': _get_uom_id,
756 'uom_po_id': _get_uom_id,
759 'categ_id' : _default_category,
764 def _check_uom(self, cursor, user, ids, context=None):
765 for product in self.browse(cursor, user, ids, context=context):
766 if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
770 def _check_uos(self, cursor, user, ids, context=None):
771 for product in self.browse(cursor, user, ids, context=context):
773 and product.uos_id.category_id.id \
774 == product.uom_id.category_id.id:
779 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
782 def name_get(self, cr, user, ids, context=None):
785 if 'partner_id' in context:
787 return super(product_template, self).name_get(cr, user, ids, context)
793 class product_product(osv.osv):
794 _name = "product.product"
795 _description = "Product"
796 _inherits = {'product.template': 'product_tmpl_id'}
797 _inherit = ['mail.thread']
798 _order = 'default_code,name_template'
800 def _product_price(self, cr, uid, ids, name, arg, context=None):
801 plobj = self.pool.get('product.pricelist')
805 quantity = context.get('quantity') or 1.0
806 pricelist = context.get('pricelist', False)
807 partner = context.get('partner', False)
809 # Support context pricelists specified as display_name or ID for compatibility
810 if isinstance(pricelist, basestring):
811 pricelist_ids = plobj.name_search(
812 cr, uid, pricelist, operator='=', context=context, limit=1)
813 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
815 if isinstance(pricelist, (int, long)):
816 products = self.browse(cr, uid, ids, context=context)
817 qtys = map(lambda x: (x, quantity, partner), products)
818 pl = plobj.browse(cr, uid, pricelist, context=context)
819 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
821 res[id] = price.get(id, 0.0)
823 res.setdefault(id, 0.0)
826 def view_header_get(self, cr, uid, view_id, view_type, context=None):
829 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
830 if (context.get('categ_id', False)):
831 return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
834 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
835 product_uom_obj = self.pool.get('product.uom')
836 res = dict.fromkeys(ids, 0.0)
838 for product in self.browse(cr, uid, ids, context=context):
840 uom = product.uos_id or product.uom_id
841 res[product.id] = product_uom_obj._compute_price(cr, uid,
842 uom.id, product.list_price, context['uom'])
844 res[product.id] = product.list_price
845 res[product.id] = res[product.id] + product.price_extra
849 def _set_product_lst_price(self, cr, uid, id, name, value, args, context=None):
850 product_uom_obj = self.pool.get('product.uom')
852 product = self.browse(cr, uid, id, context=context)
854 uom = product.uos_id or product.uom_id
855 value = product_uom_obj._compute_price(cr, uid,
856 context['uom'], value, uom.id)
857 value = value - product.price_extra
859 return product.write({'list_price': value})
861 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
862 for supinfo in product.seller_ids:
863 if supinfo.name.id == partner_id:
864 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
865 res = {'code': product.default_code, 'name': product.name}
868 def _product_code(self, cr, uid, ids, name, arg, context=None):
872 for p in self.browse(cr, uid, ids, context=context):
873 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
876 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
880 for p in self.browse(cr, uid, ids, context=context):
881 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
883 data['code'] = p.code
885 data['name'] = p.name
886 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
889 def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
890 return dict.fromkeys(ids, True)
892 def _get_name_template_ids(self, cr, uid, ids, context=None):
894 template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
895 for el in template_ids:
899 def _get_image_variant(self, cr, uid, ids, name, args, context=None):
900 result = dict.fromkeys(ids, False)
901 for obj in self.browse(cr, uid, ids, context=context):
902 result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
905 def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
906 image = tools.image_resize_image_big(value)
907 res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
908 product = self.browse(cr, uid, id, context=context)
909 if not product.product_tmpl_id.image:
910 product.write({'image_variant': None})
911 product.product_tmpl_id.write({'image': image})
914 def _get_price_extra(self, cr, uid, ids, name, args, context=None):
915 result = dict.fromkeys(ids, False)
916 for product in self.browse(cr, uid, ids, context=context):
918 for variant_id in product.attribute_value_ids:
919 for price_id in variant_id.price_ids:
920 if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
921 price_extra += price_id.price_extra
922 result[product.id] = price_extra
926 'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
927 'price_extra': fields.function(_get_price_extra, type='float', string='Variant Extra Price', help="This is the sum of the extra price of all attributes"),
928 'lst_price': fields.function(_product_lst_price, fnct_inv=_set_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
929 'code': fields.function(_product_code, type='char', string='Internal Reference'),
930 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
931 'default_code' : fields.char('Internal Reference', select=True),
932 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
933 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True, auto_join=True),
934 'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
935 'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
936 'product.template': (_get_name_template_ids, ['name'], 10),
937 'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
939 'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
940 'is_product_variant': fields.function( _is_product_variant_impl, type='boolean', string='Is product variant'),
942 # image: all image fields are base64 encoded and PIL-supported
943 'image_variant': fields.binary("Variant Image",
944 help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
946 'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
947 string="Big-sized image", type="binary",
948 help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
949 "resized as a 1024x1024px image, with aspect ratio preserved."),
950 'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
951 string="Small-sized image", type="binary",
952 help="Image of the product variant (Small-sized image of product template if false)."),
953 'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
954 string="Medium-sized image", type="binary",
955 help="Image of the product variant (Medium-sized image of product template if false)."),
963 def unlink(self, cr, uid, ids, context=None):
965 unlink_product_tmpl_ids = []
966 for product in self.browse(cr, uid, ids, context=context):
967 # Check if product still exists, in case it has been unlinked by unlinking its template
968 if not product.exists():
970 tmpl_id = product.product_tmpl_id.id
971 # Check if the product is last product of this template
972 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
973 if not other_product_ids:
974 unlink_product_tmpl_ids.append(tmpl_id)
975 unlink_ids.append(product.id)
976 res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
977 # delete templates after calling super, as deleting template could lead to deleting
978 # products due to ondelete='cascade'
979 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
982 def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
983 if uom_id and uom_po_id:
984 uom_obj=self.pool.get('product.uom')
985 uom=uom_obj.browse(cursor,user,[uom_id])[0]
986 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
987 if uom.category_id.id != uom_po.category_id.id:
988 return {'value': {'uom_po_id': uom_id}}
991 def _check_ean_key(self, cr, uid, ids, context=None):
992 for product in self.read(cr, uid, ids, ['ean13'], context=context):
993 if not check_ean(product['ean13']):
997 _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
999 def on_order(self, cr, uid, ids, orderline, quantity):
1002 def name_get(self, cr, user, ids, context=None):
1005 if isinstance(ids, (int, long)):
1011 name = d.get('name','')
1012 code = context.get('display_default_code', True) and d.get('default_code',False) or False
1014 name = '[%s] %s' % (code,name)
1015 return (d['id'], name)
1017 partner_id = context.get('partner_id', False)
1019 partner_ids = [partner_id, self.pool['res.partner'].browse(cr, user, partner_id, context=context).commercial_partner_id.id]
1023 # all user don't have access to seller and partner
1024 # check access and use superuser
1025 self.check_access_rights(cr, user, "read")
1026 self.check_access_rule(cr, user, ids, "read", context=context)
1029 for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
1030 variant = ", ".join([v.name for v in product.attribute_value_ids])
1031 name = variant and "%s (%s)" % (product.name, variant) or product.name
1034 sellers = filter(lambda x: x.name.id in partner_ids, product.seller_ids)
1037 seller_variant = s.product_name and "%s (%s)" % (s.product_name, variant) or False
1040 'name': seller_variant or name,
1041 'default_code': s.product_code or product.default_code,
1043 result.append(_name_get(mydict))
1048 'default_code': product.default_code,
1050 result.append(_name_get(mydict))
1053 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
1059 positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
1061 if operator in positive_operators:
1062 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
1064 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
1065 if not ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
1066 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1067 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1068 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1069 # Performing a quick memory merge of ids in Python will give much better performance
1070 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1071 if not limit or len(ids) < limit:
1072 # we may underrun the limit because of dupes in the results, that's fine
1073 limit2 = (limit - len(ids)) if limit else False
1074 ids.update(self.search(cr, user, args + [('name', operator, name), ('id', 'not in', list(ids))], limit=limit2, context=context))
1076 elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
1077 ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
1078 if not ids and operator in positive_operators:
1079 ptrn = re.compile('(\[(.*?)\])')
1080 res = ptrn.search(name)
1082 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1083 # still no results, partner in context: search on supplier info as last hope to find something
1084 if not ids and context.get('partner_id'):
1085 supplier_ids = self.pool['product.supplierinfo'].search(
1087 ('name', '=', context.get('partner_id')),
1089 ('product_code', operator, name),
1090 ('product_name', operator, name)
1093 ids = self.search(cr, user, [('product_tmpl_id.seller_ids', 'in', supplier_ids)], limit=limit, context=context)
1095 ids = self.search(cr, user, args, limit=limit, context=context)
1096 result = self.name_get(cr, user, ids, context=context)
1100 # Could be overrided for variants matrices prices
1102 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1103 products = self.browse(cr, uid, ids, context=context)
1104 return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1106 def copy(self, cr, uid, id, default=None, context=None):
1110 product = self.browse(cr, uid, id, context)
1111 if context.get('variant'):
1112 # if we copy a variant or create one, we keep the same template
1113 default['product_tmpl_id'] = product.product_tmpl_id.id
1114 elif 'name' not in default:
1115 default['name'] = _("%s (copy)") % (product.name,)
1117 return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1119 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1122 if context.get('search_default_categ_id'):
1123 args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1124 return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1126 def open_product_template(self, cr, uid, ids, context=None):
1127 """ Utility method used to add an "Open Template" button in product views """
1128 product = self.browse(cr, uid, ids[0], context=context)
1129 return {'type': 'ir.actions.act_window',
1130 'res_model': 'product.template',
1131 'view_mode': 'form',
1132 'res_id': product.product_tmpl_id.id,
1135 def create(self, cr, uid, vals, context=None):
1138 ctx = dict(context or {}, create_product_product=True)
1139 return super(product_product, self).create(cr, uid, vals, context=ctx)
1143 def need_procurement(self, cr, uid, ids, context=None):
1146 def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
1148 Computes product's invoicing quantity in UoS from quantity in UoM.
1149 Takes into account the
1150 :param uom: Source unit
1151 :param qty: Source quantity
1152 :param uos: Target UoS unit.
1154 if not uom or not qty or not uos:
1156 uom_obj = self.pool['product.uom']
1157 product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
1158 product = self.browse(cr, uid, product_id, context=context)
1159 if isinstance(uos, (int, long)):
1160 uos = uom_obj.browse(cr, uid, uos, context=context)
1161 if isinstance(uom, (int, long)):
1162 uom = uom_obj.browse(cr, uid, uom, context=context)
1163 if product.uos_id: # Product has UoS defined
1164 # We cannot convert directly between units even if the units are of the same category
1165 # as we need to apply the conversion coefficient which is valid only between quantities
1166 # in product's default UoM/UoS
1167 qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id) # qty in product's default UoM
1168 qty_default_uos = qty_default_uom * product.uos_coeff
1169 return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
1171 return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
1175 class product_packaging(osv.osv):
1176 _name = "product.packaging"
1177 _description = "Packaging"
1181 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1182 'name' : fields.text('Description'),
1183 'qty' : fields.float('Quantity by Package',
1184 help="The total number of products you can put by pallet or box."),
1185 'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1186 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1187 'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1188 'rows' : fields.integer('Number of Layers', required=True,
1189 help='The number of layers on a pallet or box'),
1190 'product_tmpl_id' : fields.many2one('product.template', 'Product', select=1, ondelete='cascade', required=True),
1191 'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1192 'code' : fields.char('Code', help="The code of the transport unit."),
1193 'weight': fields.float('Total Package Weight',
1194 help='The weight of a full package, pallet or box.'),
1197 def _check_ean_key(self, cr, uid, ids, context=None):
1198 for pack in self.browse(cr, uid, ids, context=context):
1199 if not check_ean(pack.ean):
1203 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1205 def name_get(self, cr, uid, ids, context=None):
1209 for pckg in self.browse(cr, uid, ids, context=context):
1210 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1211 p_name += pckg.ul.name
1212 res.append((pckg.id,p_name))
1215 def _get_1st_ul(self, cr, uid, context=None):
1216 cr.execute('select id from product_ul order by id asc limit 1')
1218 return (res and res[0]) or False
1227 salt = '31' * 6 + '3'
1229 for ean_part, salt_part in zip(ean, salt):
1230 sum += int(ean_part) * int(salt_part)
1231 return (10 - (sum % 10)) % 10
1232 checksum = staticmethod(checksum)
1236 class product_supplierinfo(osv.osv):
1237 _name = "product.supplierinfo"
1238 _description = "Information about a product supplier"
1239 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1241 for supplier_info in self.browse(cr, uid, ids, context=context):
1242 for field in fields:
1243 result[supplier_info.id] = {field:False}
1244 qty = supplier_info.min_qty
1245 result[supplier_info.id]['qty'] = qty
1249 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1250 'product_name': fields.char('Supplier Product Name', help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
1251 'product_code': fields.char('Supplier Product Code', help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
1252 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1253 'product_uom': fields.related('product_tmpl_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
1254 '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."),
1255 '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."),
1256 'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1257 'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the receipt of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
1258 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist', copy=True),
1259 'company_id':fields.many2one('res.company', string='Company',select=1),
1265 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1271 class pricelist_partnerinfo(osv.osv):
1272 _name = 'pricelist.partnerinfo'
1274 'name': fields.char('Description'),
1275 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1276 '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."),
1277 '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"),
1279 _order = 'min_quantity asc'
1281 class res_currency(osv.osv):
1282 _inherit = 'res.currency'
1284 def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1285 cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1286 digits = cr.fetchone()
1287 if digits and len(digits):
1289 main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1290 for currency_id in ids:
1291 if currency_id == main_currency.id:
1292 if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1297 (_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']),
1300 class decimal_precision(osv.osv):
1301 _inherit = 'decimal.precision'
1303 def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1304 cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1306 if res and len(res):
1307 account_precision_id, digits = res
1308 main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1309 for decimal_precision in ids:
1310 if decimal_precision == account_precision_id:
1311 if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1316 (_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']),
1319 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: