1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from osv import osv, fields
23 import decimal_precision as dp
26 from _common import rounding
29 from tools.translate import _
34 def check_ean(eancode):
37 if len(eancode) <> 13:
47 reversevalue = eanvalue[::-1]
48 finalean=reversevalue[1:]
50 for i in range(len(finalean)):
52 oddsum += int(finalean[i])
54 evensum += int(finalean[i])
55 total=(oddsum * 3) + evensum
57 check = int(10 - math.ceil(total % 10.0)) %10
59 if check != int(eancode[-1]):
62 #----------------------------------------------------------
64 #----------------------------------------------------------
66 class product_uom_categ(osv.osv):
67 _name = 'product.uom.categ'
68 _description = 'Product uom categ'
70 'name': fields.char('Name', size=64, required=True, translate=True),
74 class product_uom(osv.osv):
76 _description = 'Product Unit of Measure'
78 def _compute_factor_inv(self, factor):
79 return factor and (1.0 / factor) or 0.0
81 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
83 for uom in self.browse(cursor, user, ids, context=context):
84 res[uom.id] = self._compute_factor_inv(uom.factor)
87 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
88 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
90 def create(self, cr, uid, data, context=None):
91 if 'factor_inv' in data:
92 if data['factor_inv'] <> 1:
93 data['factor'] = self._compute_factor_inv(data['factor_inv'])
94 del(data['factor_inv'])
95 return super(product_uom, self).create(cr, uid, data, context)
99 'name': fields.char('Name', size=64, required=True, translate=True),
100 'category_id': fields.many2one('product.uom.categ', 'Unit of Measure Category', required=True, ondelete='cascade',
101 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
102 'factor': fields.float('Ratio', required=True,digits=(12, 12),
103 help='How many times this Unit of Measure is smaller than the reference Unit of Measure in this category:\n'\
104 '1 * (reference unit) = ratio * (this unit)'),
105 'factor_inv': fields.function(_factor_inv, digits=(12,12),
106 fnct_inv=_factor_inv_write,
108 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
109 '1 * (this unit) = ratio * (reference unit)', required=True),
110 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
111 help="The computed quantity will be a multiple of this value. "\
112 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
113 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
114 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
115 ('reference','Reference Unit of Measure for this category'),
116 ('smaller','Smaller than the reference Unit of Measure')],'Unit of Measure Type', required=1),
122 'uom_type': 'reference',
126 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
129 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
130 if not from_uom_id or not qty or not to_uom_id:
132 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
133 if uoms[0].id == from_uom_id:
134 from_unit, to_unit = uoms[0], uoms[-1]
136 from_unit, to_unit = uoms[-1], uoms[0]
137 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
139 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
142 if from_unit.category_id.id <> to_unit.category_id.id:
143 if context.get('raise-exception', True):
144 raise osv.except_osv(_('Error !'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
147 amount = qty / from_unit.factor
149 amount = rounding(amount * to_unit.factor, to_unit.rounding)
152 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
153 if not from_uom_id or not price or not to_uom_id:
155 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
156 if uoms[0].id == from_uom_id:
157 from_unit, to_unit = uoms[0], uoms[-1]
159 from_unit, to_unit = uoms[-1], uoms[0]
160 if from_unit.category_id.id <> to_unit.category_id.id:
162 amount = price * from_unit.factor
164 amount = amount / to_unit.factor
167 def onchange_type(self, cursor, user, ids, value):
168 if value == 'reference':
169 return {'value': {'factor': 1, 'factor_inv': 1}}
172 def write(self, cr, uid, ids, vals, context=None):
173 if 'category_id' in vals:
174 for uom in self.browse(cr, uid, ids, context=context):
175 if uom.category_id.id != vals['category_id']:
176 raise osv.except_osv(_('Warning'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
177 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
182 class product_ul(osv.osv):
184 _description = "Shipping Unit"
186 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
187 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
192 #----------------------------------------------------------
194 #----------------------------------------------------------
195 class product_category(osv.osv):
197 def name_get(self, cr, uid, ids, context=None):
200 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
203 name = record['name']
204 if record['parent_id']:
205 name = record['parent_id'][1]+' / '+name
206 res.append((record['id'], name))
209 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
210 res = self.name_get(cr, uid, ids, context=context)
213 _name = "product.category"
214 _description = "Product Category"
216 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
217 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
218 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
219 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
220 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
221 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
222 'parent_left': fields.integer('Left Parent', select=1),
223 'parent_right': fields.integer('Right Parent', select=1),
228 'type' : lambda *a : 'normal',
231 _parent_name = "parent_id"
233 _parent_order = 'sequence, name'
234 _order = 'parent_left'
236 def _check_recursion(self, cr, uid, ids, context=None):
239 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
240 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
247 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
249 def child_get(self, cr, uid, ids):
255 #----------------------------------------------------------
257 #----------------------------------------------------------
258 class product_template(osv.osv):
259 _name = "product.template"
260 _description = "Product Template"
263 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
264 'product_manager': fields.many2one('res.users','Product Manager',help="Responsible for product."),
265 'description': fields.text('Description',translate=True),
266 'description_purchase': fields.text('Purchase Description',translate=True),
267 'description_sale': fields.text('Sale Description',translate=True),
268 'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumable are product where you don't manage stock."),
269 'supply_method': fields.selection([('produce','Manufacture'),('buy','Buy')], 'Supply Method', required=True, help="Produce will generate production order or tasks, according to the product type. Buy will trigger purchase orders when requested."),
270 'sale_delay': fields.float('Customer Lead Time', help="This is the average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
271 'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. This is only for the production order and, if it is a multi-level bill of material, it's only for the level of this product. Different lead times will be summed for all levels and purchase orders."),
272 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
273 'rental': fields.boolean('Can be Rent'),
274 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
275 'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Sale Price'), help="Base price for computing the customer price. Sometimes called the catalog price."),
276 'standard_price': fields.float('Cost Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="Product's cost for accounting stock valuation. It is the base price for the supplier price.", groups="base.group_user"),
277 'volume': fields.float('Volume', help="The volume in m3."),
278 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
279 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
280 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
281 help="Standard Price: the cost price is fixed and recomputed periodically (usually at the end of the year), Average Price: the cost price is recomputed at each reception of products."),
282 'warranty': fields.float('Warranty (months)'),
283 'sale_ok': fields.boolean('Can be Sold', help="Determines if the product can be visible in the list of product within a selection from a sale order line."),
284 'purchase_ok': fields.boolean('Can be Purchased', help="Determine if the product is visible in the list of products within a selection from a purchase order line."),
285 'state': fields.selection([('',''),
286 ('draft', 'In Development'),
287 ('sellable','Normal'),
288 ('end','End of Lifecycle'),
289 ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
290 'uom_id': fields.many2one('product.uom', 'Default Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
291 '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."),
292 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
293 help='Used by companies that manage two units of measure: invoicing and inventory management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default Unit of Measure.'),
294 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
295 help='Coefficient to convert Unit of Measure to UOS\n'
296 ' uos = uom * coeff'),
297 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
298 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
299 'loc_rack': fields.char('Rack', size=16),
300 'loc_row': fields.char('Row', size=16),
301 'loc_case': fields.char('Case', size=16),
302 'company_id': fields.many2one('res.company', 'Company', select=1),
305 def _get_uom_id(self, cr, uid, *args):
306 cr.execute('select id from product_uom order by id limit 1')
308 return res and res[0] or False
310 def _default_category(self, cr, uid, context=None):
313 if 'categ_id' in context and context['categ_id']:
314 return context['categ_id']
315 md = self.pool.get('ir.model.data')
318 res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
323 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
325 return {'value': {'uom_po_id': uom_id}}
328 def write(self, cr, uid, ids, vals, context=None):
329 if 'uom_po_id' in vals:
330 new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
331 for product in self.browse(cr, uid, ids, context=context):
332 old_uom = product.uom_po_id
333 if old_uom.category_id.id != new_uom.category_id.id:
334 raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may desactivate this product from the 'Procurement & Locations' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
335 return super(product_template, self).write(cr, uid, ids, vals, context=context)
338 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
339 'list_price': lambda *a: 1,
340 'cost_method': lambda *a: 'standard',
341 'supply_method': lambda *a: 'buy',
342 'standard_price': lambda *a: 1,
343 'sale_ok': lambda *a: 1,
344 'sale_delay': lambda *a: 7,
345 'produce_delay': lambda *a: 1,
346 'purchase_ok': lambda *a: 1,
347 'procure_method': lambda *a: 'make_to_stock',
348 'uom_id': _get_uom_id,
349 'uom_po_id': _get_uom_id,
350 'uos_coeff' : lambda *a: 1.0,
351 'mes_type' : lambda *a: 'fixed',
352 'categ_id' : _default_category,
353 'type' : lambda *a: 'consu',
356 def _check_uom(self, cursor, user, ids, context=None):
357 for product in self.browse(cursor, user, ids, context=context):
358 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
362 def _check_uos(self, cursor, user, ids, context=None):
363 for product in self.browse(cursor, user, ids, context=context):
365 and product.uos_id.category_id.id \
366 == product.uom_id.category_id.id:
371 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
374 def name_get(self, cr, user, ids, context=None):
377 if 'partner_id' in context:
379 return super(product_template, self).name_get(cr, user, ids, context)
383 class product_product(osv.osv):
384 def view_header_get(self, cr, uid, view_id, view_type, context=None):
387 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
388 if (context.get('categ_id', False)):
389 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
392 def _product_price(self, cr, uid, ids, name, arg, context=None):
396 quantity = context.get('quantity') or 1.0
397 pricelist = context.get('pricelist', False)
398 partner = context.get('partner', False)
402 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
407 res.setdefault(id, 0.0)
410 def _get_product_available_func(states, what):
411 def _product_available(self, cr, uid, ids, name, arg, context=None):
412 return {}.fromkeys(ids, 0.0)
413 return _product_available
415 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
416 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
417 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
418 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
420 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
422 product_uom_obj = self.pool.get('product.uom')
424 res.setdefault(id, 0.0)
425 for product in self.browse(cr, uid, ids, context=context):
427 uom = product.uos_id or product.uom_id
428 res[product.id] = product_uom_obj._compute_price(cr, uid,
429 uom.id, product.list_price, context['uom'])
431 res[product.id] = product.list_price
432 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
435 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
436 for supinfo in product.seller_ids:
437 if supinfo.name.id == partner_id:
438 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
439 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
442 def _product_code(self, cr, uid, ids, name, arg, context=None):
446 for p in self.browse(cr, uid, ids, context=context):
447 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
450 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
454 for p in self.browse(cr, uid, ids, context=context):
455 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
456 if not data['variants']:
457 data['variants'] = p.variants
459 data['code'] = p.code
461 data['name'] = p.name
462 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
463 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
467 def _get_main_product_supplier(self, cr, uid, product, context=None):
468 """Determines the main (best) product supplier for ``product``,
469 returning the corresponding ``supplierinfo`` record, or False
470 if none were found. The default strategy is to select the
471 supplier with the highest priority (i.e. smallest sequence).
473 :param browse_record product: product to supply
474 :rtype: product.supplierinfo browse_record or False
476 sellers = [(seller_info.sequence, seller_info)
477 for seller_info in product.seller_ids or []
478 if seller_info and isinstance(seller_info.sequence, (int, long))]
479 return sellers and sellers[0][1] or False
481 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
483 for product in self.browse(cr, uid, ids, context=context):
484 main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
485 result[product.id] = {
486 'seller_info_id': main_supplier and main_supplier.id or False,
487 'seller_delay': main_supplier and main_supplier.delay or 1,
488 'seller_qty': main_supplier and main_supplier.qty or 0.0,
489 'seller_id': main_supplier and main_supplier.name.id or False
494 def _get_image(self, cr, uid, ids, name, args, context=None):
495 result = dict.fromkeys(ids, False)
496 for obj in self.browse(cr, uid, ids, context=context):
497 result[obj.id] = tools.image_get_resized_images(obj.image)
500 def _set_image(self, cr, uid, id, name, value, args, context=None):
501 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
504 'active': lambda *a: 1,
505 'price_extra': lambda *a: 0.0,
506 'price_margin': lambda *a: 1.0,
510 _name = "product.product"
511 _description = "Product"
512 _table = "product_product"
513 _inherits = {'product.template': 'product_tmpl_id'}
514 _inherit = ['mail.thread']
515 _order = 'default_code,name_template'
517 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
518 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
519 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
520 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
521 'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
522 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
523 'code': fields.function(_product_code, type='char', string='Reference'),
524 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
525 'default_code' : fields.char('Reference', size=64, select=True),
526 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
527 'variants': fields.char('Variants', size=64),
528 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
529 'ean13': fields.char('EAN13', size=13, help="The numbers encoded in EAN-13 bar codes are product identification numbers."),
530 '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."),
531 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
532 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
533 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
534 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True, select=True),
535 'color': fields.integer('Color Index'),
536 'image': fields.binary("Image",
537 help="This field holds the image used for the product. "\
538 "The image is base64 encoded, and PIL-supported. "\
539 "It is limited to a 1024x1024 px image."),
540 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
541 string="Medium-sized image", type="binary", multi="_get_image",
543 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
545 help="Medium-sized image of the product. It is automatically "\
546 "resized as a 180x180 px image, with aspect ratio preserved. "\
547 "Use this field in form views or some kanban views."),
548 'image_small': fields.function(_get_image, fnct_inv=_set_image,
549 string="Small-sized image", type="binary", multi="_get_image",
551 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
553 help="Small-sized image of the product. It is automatically "\
554 "resized as a 50x50 px image, with aspect ratio preserved. "\
555 "Use this field anywhere a small image is required."),
556 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", multi="seller_info"),
557 '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."),
558 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
559 '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"),
562 def create(self, cr, uid, vals, context=None):
563 obj_id = super(product_product, self).create(cr, uid, vals, context=context)
564 self.create_send_note(cr, uid, [obj_id], context=context)
567 def create_send_note(self, cr, uid, ids, context=None):
568 return self.message_append_note(cr, uid, ids, body=_("Product has been <b>created</b>."), context=context)
570 def unlink(self, cr, uid, ids, context=None):
572 unlink_product_tmpl_ids = []
573 for product in self.browse(cr, uid, ids, context=context):
574 tmpl_id = product.product_tmpl_id.id
575 # Check if the product is last product of this template
576 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
577 if not other_product_ids:
578 unlink_product_tmpl_ids.append(tmpl_id)
579 unlink_ids.append(product.id)
580 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
581 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
583 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
584 if uom_id and uom_po_id:
585 uom_obj=self.pool.get('product.uom')
586 uom=uom_obj.browse(cursor,user,[uom_id])[0]
587 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
588 if uom.category_id.id != uom_po.category_id.id:
589 return {'value': {'uom_po_id': uom_id}}
592 def _check_ean_key(self, cr, uid, ids, context=None):
593 for product in self.read(cr, uid, ids, ['ean13'], context=context):
594 res = check_ean(product['ean13'])
597 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
599 def on_order(self, cr, uid, ids, orderline, quantity):
602 def name_get(self, cr, user, ids, context=None):
608 name = d.get('name','')
609 code = d.get('default_code',False)
611 name = '[%s] %s' % (code,name)
612 if d.get('variants'):
613 name = name + ' - %s' % (d['variants'],)
614 return (d['id'], name)
616 partner_id = context.get('partner_id', False)
619 for product in self.browse(cr, user, ids, context=context):
620 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
625 'name': s.product_name or product.name,
626 'default_code': s.product_code or product.default_code,
627 'variants': product.variants
629 result.append(_name_get(mydict))
633 'name': product.name,
634 'default_code': product.default_code,
635 'variants': product.variants
637 result.append(_name_get(mydict))
640 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
644 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
646 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
648 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
649 # on a database with thousands of matching products, due to the huge merge+unique needed for the
650 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
651 # Performing a quick memory merge of ids in Python will give much better performance
653 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
655 # we may underrun the limit because of dupes in the results, that's fine
656 ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit-len(ids)), context=context))
659 ptrn = re.compile('(\[(.*?)\])')
660 res = ptrn.search(name)
662 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
664 ids = self.search(cr, user, args, limit=limit, context=context)
665 result = self.name_get(cr, user, ids, context=context)
669 # Could be overrided for variants matrices prices
671 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
675 if 'currency_id' in context:
676 pricetype_obj = self.pool.get('product.price.type')
677 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
678 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
681 product_uom_obj = self.pool.get('product.uom')
682 for product in self.browse(cr, uid, ids, context=context):
683 res[product.id] = product[ptype] or 0.0
684 if ptype == 'list_price':
685 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
688 uom = product.uom_id or product.uos_id
689 res[product.id] = product_uom_obj._compute_price(cr, uid,
690 uom.id, res[product.id], context['uom'])
691 # Convert from price_type currency to asked one
692 if 'currency_id' in context:
693 # Take the price_type currency from the product field
694 # This is right cause a field cannot be in more than one currency
695 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
696 context['currency_id'], res[product.id],context=context)
700 def copy(self, cr, uid, id, default=None, context=None):
707 # Craft our own `<name> (copy)` in en_US (self.copy_translation()
708 # will do the other languages).
709 context_wo_lang = context.copy()
710 context_wo_lang.pop('lang', None)
711 product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
712 default = default.copy()
713 default['name'] = product['name'] + ' (copy)'
715 if context.get('variant',False):
716 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
717 'price_margin', 'price_extra']
718 data = self.read(cr, uid, id, fields=fields, context=context)
722 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
723 and data['product_tmpl_id'][0]
725 return self.create(cr, uid, data)
727 return super(product_product, self).copy(cr, uid, id, default=default,
731 class product_packaging(osv.osv):
732 _name = "product.packaging"
733 _description = "Packaging"
737 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
738 'name' : fields.text('Description', size=64),
739 'qty' : fields.float('Quantity by Package',
740 help="The total number of products you can put by pallet or box."),
741 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
742 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
743 'rows' : fields.integer('Number of Layers', required=True,
744 help='The number of layers on a pallet or box'),
745 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
746 'ean' : fields.char('EAN', size=14,
747 help="The EAN code of the package unit."),
748 'code' : fields.char('Code', size=14,
749 help="The code of the transport unit."),
750 'weight': fields.float('Total Package Weight',
751 help='The weight of a full package, pallet or box.'),
752 'weight_ul': fields.float('Empty Package Weight',
753 help='The weight of the empty UL'),
754 'height': fields.float('Height', help='The height of the package'),
755 'width': fields.float('Width', help='The width of the package'),
756 'length': fields.float('Length', help='The length of the package'),
760 def _check_ean_key(self, cr, uid, ids, context=None):
761 for pack in self.browse(cr, uid, ids, context=context):
762 res = check_ean(pack.ean)
765 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
767 def name_get(self, cr, uid, ids, context=None):
771 for pckg in self.browse(cr, uid, ids, context=context):
772 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
773 p_name += pckg.ul.name
774 res.append((pckg.id,p_name))
777 def _get_1st_ul(self, cr, uid, context=None):
778 cr.execute('select id from product_ul order by id asc limit 1')
780 return (res and res[0]) or False
783 'rows' : lambda *a : 3,
784 'sequence' : lambda *a : 1,
789 salt = '31' * 6 + '3'
791 for ean_part, salt_part in zip(ean, salt):
792 sum += int(ean_part) * int(salt_part)
793 return (10 - (sum % 10)) % 10
794 checksum = staticmethod(checksum)
799 class product_supplierinfo(osv.osv):
800 _name = "product.supplierinfo"
801 _description = "Information about a product supplier"
802 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
804 product_uom_pool = self.pool.get('product.uom')
805 for supplier_info in self.browse(cr, uid, ids, context=context):
807 result[supplier_info.id] = {field:False}
808 qty = supplier_info.min_qty
809 result[supplier_info.id]['qty'] = qty
813 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
814 '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."),
815 '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."),
816 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
817 '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."),
818 '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."),
819 '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."),
820 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
821 '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."),
822 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
823 'company_id':fields.many2one('res.company','Company',select=1),
826 'qty': lambda *a: 0.0,
827 'sequence': lambda *a: 1,
828 'delay': lambda *a: 1,
829 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
831 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
833 Calculate price from supplier pricelist.
834 @param supplier_ids: Ids of res.partner object.
835 @param product_id: Id of product.
836 @param product_qty: specify quantity to purchase.
838 if type(supplier_ids) in (int,long,):
839 supplier_ids = [supplier_ids]
841 product_pool = self.pool.get('product.product')
842 partner_pool = self.pool.get('res.partner')
843 pricelist_pool = self.pool.get('product.pricelist')
844 currency_pool = self.pool.get('res.currency')
845 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
846 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
847 # Compute price from standard price of product
848 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
850 # Compute price from Purchase pricelist of supplier
851 pricelist_id = supplier.property_product_pricelist_purchase.id
853 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
854 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
856 # Compute price from supplier pricelist which are in Supplier Information
857 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
858 if supplier_info_ids:
859 cr.execute('SELECT * ' \
860 'FROM pricelist_partnerinfo ' \
861 'WHERE suppinfo_id IN %s' \
862 'AND min_quantity <= %s ' \
863 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
864 res2 = cr.dictfetchone()
866 price = res2['price']
867 res[supplier.id] = price
870 product_supplierinfo()
873 class pricelist_partnerinfo(osv.osv):
874 _name = 'pricelist.partnerinfo'
876 'name': fields.char('Description', size=64),
877 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
878 '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."),
879 'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="This price will be considered as a price for the supplier Unit of Measure if any or the default Unit of Measure of the product otherwise"),
881 _order = 'min_quantity asc'
882 pricelist_partnerinfo()
883 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: