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
28 from tools.translate import _
33 def check_ean(eancode):
36 if len(eancode) <> 13:
46 reversevalue = eanvalue[::-1]
47 finalean=reversevalue[1:]
49 for i in range(len(finalean)):
51 oddsum += int(finalean[i])
53 evensum += int(finalean[i])
54 total=(oddsum * 3) + evensum
56 check = int(10 - math.ceil(total % 10.0)) %10
58 if check != int(eancode[-1]):
61 #----------------------------------------------------------
63 #----------------------------------------------------------
65 class product_uom_categ(osv.osv):
66 _name = 'product.uom.categ'
67 _description = 'Product uom categ'
69 'name': fields.char('Name', size=64, required=True, translate=True),
73 class product_uom(osv.osv):
75 _description = 'Product Unit of Measure'
77 def _compute_factor_inv(self, factor):
78 return factor and (1.0 / factor) or 0.0
80 def _factor_inv(self, cursor, user, ids, name, arg, context=None):
82 for uom in self.browse(cursor, user, ids, context=context):
83 res[uom.id] = self._compute_factor_inv(uom.factor)
86 def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
87 return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
89 def create(self, cr, uid, data, context=None):
90 if 'factor_inv' in data:
91 if data['factor_inv'] <> 1:
92 data['factor'] = self._compute_factor_inv(data['factor_inv'])
93 del(data['factor_inv'])
94 return super(product_uom, self).create(cr, uid, data, context)
98 'name': fields.char('Name', size=64, required=True, translate=True),
99 'category_id': fields.many2one('product.uom.categ', 'Unit of Measure Category', required=True, ondelete='cascade',
100 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
101 'factor': fields.float('Ratio', required=True,digits=(12, 12),
102 help='How many times this Unit of Measure is smaller than the reference Unit of Measure in this category:\n'\
103 '1 * (reference unit) = ratio * (this unit)'),
104 'factor_inv': fields.function(_factor_inv, digits=(12,12),
105 fnct_inv=_factor_inv_write,
107 help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
108 '1 * (this unit) = ratio * (reference unit)', required=True),
109 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
110 help="The computed quantity will be a multiple of this value. "\
111 "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
112 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
113 'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
114 ('reference','Reference Unit of Measure for this category'),
115 ('smaller','Smaller than the reference Unit of Measure')],'Unit of Measure Type', required=1),
121 'uom_type': 'reference',
125 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
128 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
129 if not from_uom_id or not qty or not to_uom_id:
131 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
132 if uoms[0].id == from_uom_id:
133 from_unit, to_unit = uoms[0], uoms[-1]
135 from_unit, to_unit = uoms[-1], uoms[0]
136 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
138 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
141 if from_unit.category_id.id <> to_unit.category_id.id:
142 if context.get('raise-exception', True):
143 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,))
146 amount = qty / from_unit.factor
148 amount = rounding(amount * to_unit.factor, to_unit.rounding)
151 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
152 if not from_uom_id or not price or not to_uom_id:
154 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
155 if uoms[0].id == from_uom_id:
156 from_unit, to_unit = uoms[0], uoms[-1]
158 from_unit, to_unit = uoms[-1], uoms[0]
159 if from_unit.category_id.id <> to_unit.category_id.id:
161 amount = price * from_unit.factor
163 amount = amount / to_unit.factor
166 def onchange_type(self, cursor, user, ids, value):
167 if value == 'reference':
168 return {'value': {'factor': 1, 'factor_inv': 1}}
171 def write(self, cr, uid, ids, vals, context=None):
172 if 'category_id' in vals:
173 for uom in self.browse(cr, uid, ids, context=context):
174 if uom.category_id.id != vals['category_id']:
175 raise osv.except_osv(_('Warning'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
176 return super(product_uom, self).write(cr, uid, ids, vals, context=context)
181 class product_ul(osv.osv):
183 _description = "Shipping Unit"
185 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
186 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
191 #----------------------------------------------------------
193 #----------------------------------------------------------
194 class product_category(osv.osv):
196 def name_get(self, cr, uid, ids, context=None):
199 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
202 name = record['name']
203 if record['parent_id']:
204 name = record['parent_id'][1]+' / '+name
205 res.append((record['id'], name))
208 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
209 res = self.name_get(cr, uid, ids, context=context)
212 _name = "product.category"
213 _description = "Product Category"
215 'name': fields.char('Name', size=64, required=True, translate=True, select=True),
216 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
217 'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
218 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
219 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
220 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
221 'parent_left': fields.integer('Left Parent', select=1),
222 'parent_right': fields.integer('Right Parent', select=1),
227 'type' : lambda *a : 'normal',
230 _parent_name = "parent_id"
232 _parent_order = 'sequence, name'
233 _order = 'parent_left'
235 def _check_recursion(self, cr, uid, ids, context=None):
238 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
239 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
246 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
248 def child_get(self, cr, uid, ids):
254 #----------------------------------------------------------
256 #----------------------------------------------------------
257 class product_template(osv.osv):
258 _name = "product.template"
259 _description = "Product Template"
261 def _get_main_product_supplier(self, cr, uid, product, context=None):
262 """Determines the main (best) product supplier for ``product``,
263 returning the corresponding ``supplierinfo`` record, or False
264 if none were found. The default strategy is to select the
265 supplier with the highest priority (i.e. smallest sequence).
267 :param browse_record product: product to supply
268 :rtype: product.supplierinfo browse_record or False
270 sellers = [(seller_info.sequence, seller_info)
271 for seller_info in product.seller_ids or []
272 if seller_info and isinstance(seller_info.sequence, (int, long))]
273 return sellers and sellers[0][1] or False
275 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
277 for product in self.browse(cr, uid, ids, context=context):
278 main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
279 result[product.id] = {
280 'seller_info_id': main_supplier and main_supplier.id or False,
281 'seller_delay': main_supplier and main_supplier.delay or 1,
282 'seller_qty': main_supplier and main_supplier.qty or 0.0,
283 'seller_id': main_supplier and main_supplier.name.id or False
288 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
289 'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
290 'description': fields.text('Description',translate=True),
291 'description_purchase': fields.text('Purchase Description',translate=True),
292 'description_sale': fields.text('Sale Description',translate=True),
293 '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."),
294 '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."),
295 '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."),
296 '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."),
297 '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."),
298 'rental': fields.boolean('Can be Rent'),
299 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
300 '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."),
301 '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"),
302 'volume': fields.float('Volume', help="The volume in m3."),
303 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
304 'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
305 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
306 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."),
307 'warranty': fields.float('Warranty (months)'),
308 '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."),
309 '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."),
310 'state': fields.selection([('',''),
311 ('draft', 'In Development'),
312 ('sellable','Normal'),
313 ('end','End of Lifecycle'),
314 ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
315 'uom_id': fields.many2one('product.uom', 'Default Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
316 '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."),
317 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
318 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.'),
319 'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
320 help='Coefficient to convert Unit of Measure to UOS\n'
321 ' uos = uom * coeff'),
322 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
323 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", multi="seller_info"),
324 '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."),
325 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
326 '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"),
327 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
328 'loc_rack': fields.char('Rack', size=16),
329 'loc_row': fields.char('Row', size=16),
330 'loc_case': fields.char('Case', size=16),
331 'company_id': fields.many2one('res.company', 'Company', select=1),
334 def _get_uom_id(self, cr, uid, *args):
335 cr.execute('select id from product_uom order by id limit 1')
337 return res and res[0] or False
339 def _default_category(self, cr, uid, context=None):
342 if 'categ_id' in context and context['categ_id']:
343 return context['categ_id']
344 md = self.pool.get('ir.model.data')
347 res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
352 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
354 return {'value': {'uom_po_id': uom_id}}
357 def write(self, cr, uid, ids, vals, context=None):
358 if 'uom_po_id' in vals:
359 new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
360 for product in self.browse(cr, uid, ids, context=context):
361 old_uom = product.uom_po_id
362 if old_uom.category_id.id != new_uom.category_id.id:
363 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,))
364 return super(product_template, self).write(cr, uid, ids, vals, context=context)
367 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
368 'list_price': lambda *a: 1,
369 'cost_method': lambda *a: 'standard',
370 'supply_method': lambda *a: 'buy',
371 'standard_price': lambda *a: 1,
372 'sale_ok': lambda *a: 1,
373 'sale_delay': lambda *a: 7,
374 'produce_delay': lambda *a: 1,
375 'purchase_ok': lambda *a: 1,
376 'procure_method': lambda *a: 'make_to_stock',
377 'uom_id': _get_uom_id,
378 'uom_po_id': _get_uom_id,
379 'uos_coeff' : lambda *a: 1.0,
380 'mes_type' : lambda *a: 'fixed',
381 'categ_id' : _default_category,
382 'type' : lambda *a: 'consu',
385 def _check_uom(self, cursor, user, ids, context=None):
386 for product in self.browse(cursor, user, ids, context=context):
387 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
391 def _check_uos(self, cursor, user, ids, context=None):
392 for product in self.browse(cursor, user, ids, context=context):
394 and product.uos_id.category_id.id \
395 == product.uom_id.category_id.id:
400 (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
403 def name_get(self, cr, user, ids, context=None):
406 if 'partner_id' in context:
408 return super(product_template, self).name_get(cr, user, ids, context)
412 class product_product(osv.osv):
413 def view_header_get(self, cr, uid, view_id, view_type, context=None):
416 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
417 if (context.get('categ_id', False)):
418 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
421 def _product_price(self, cr, uid, ids, name, arg, context=None):
425 quantity = context.get('quantity') or 1.0
426 pricelist = context.get('pricelist', False)
427 partner = context.get('partner', False)
431 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, partner=partner, context=context)[pricelist]
436 res.setdefault(id, 0.0)
439 def _get_product_available_func(states, what):
440 def _product_available(self, cr, uid, ids, name, arg, context=None):
441 return {}.fromkeys(ids, 0.0)
442 return _product_available
444 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
445 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
446 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
447 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
449 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
451 product_uom_obj = self.pool.get('product.uom')
453 res.setdefault(id, 0.0)
454 for product in self.browse(cr, uid, ids, context=context):
456 uom = product.uos_id or product.uom_id
457 res[product.id] = product_uom_obj._compute_price(cr, uid,
458 uom.id, product.list_price, context['uom'])
460 res[product.id] = product.list_price
461 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
464 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
465 for supinfo in product.seller_ids:
466 if supinfo.name.id == partner_id:
467 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
468 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
471 def _product_code(self, cr, uid, ids, name, arg, context=None):
475 for p in self.browse(cr, uid, ids, context=context):
476 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
479 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
483 for p in self.browse(cr, uid, ids, context=context):
484 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
485 if not data['variants']:
486 data['variants'] = p.variants
488 data['code'] = p.code
490 data['name'] = p.name
491 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
492 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
496 'active': lambda *a: 1,
497 'price_extra': lambda *a: 0.0,
498 'price_margin': lambda *a: 1.0,
502 _name = "product.product"
503 _description = "Product"
504 _table = "product_product"
505 _inherits = {'product.template': 'product_tmpl_id'}
506 _inherit = ['mail.thread']
507 _order = 'default_code,name_template'
509 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
510 'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
511 'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
512 'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
513 'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
514 'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
515 'code': fields.function(_product_code, type='char', string='Reference'),
516 'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
517 'default_code' : fields.char('Reference', size=64, select=True),
518 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
519 'variants': fields.char('Variants', size=64),
520 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
521 'ean13': fields.char('EAN13', size=13, help="The numbers encoded in EAN-13 bar codes are product identification numbers."),
522 '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."),
523 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
524 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
525 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
526 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True, select=True),
527 'color': fields.integer('Color Index'),
528 'product_image': fields.binary('Image'),
531 def create(self, cr, uid, vals, context=None):
532 obj_id = super(product_product, self).create(cr, uid, vals, context=context)
533 self.create_send_note(cr, uid, [obj_id], context=context)
536 def create_send_note(self, cr, uid, ids, context=None):
537 return self.message_append_note(cr, uid, ids, body=_("Product has been <b>created</b>."), context=context)
539 def unlink(self, cr, uid, ids, context=None):
541 unlink_product_tmpl_ids = []
542 for product in self.browse(cr, uid, ids, context=context):
543 tmpl_id = product.product_tmpl_id.id
544 # Check if the product is last product of this template
545 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
546 if not other_product_ids:
547 unlink_product_tmpl_ids.append(tmpl_id)
548 unlink_ids.append(product.id)
549 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
550 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
552 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
553 if uom_id and uom_po_id:
554 uom_obj=self.pool.get('product.uom')
555 uom=uom_obj.browse(cursor,user,[uom_id])[0]
556 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
557 if uom.category_id.id != uom_po.category_id.id:
558 return {'value': {'uom_po_id': uom_id}}
561 def _check_ean_key(self, cr, uid, ids, context=None):
562 for product in self.browse(cr, uid, ids, context=context):
563 res = check_ean(product.ean13)
566 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
568 def on_order(self, cr, uid, ids, orderline, quantity):
571 def name_get(self, cr, user, ids, context=None):
577 name = d.get('name','')
578 code = d.get('default_code',False)
580 name = '[%s] %s' % (code,name)
581 if d.get('variants'):
582 name = name + ' - %s' % (d['variants'],)
583 return (d['id'], name)
585 partner_id = context.get('partner_id', False)
588 for product in self.browse(cr, user, ids, context=context):
589 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
594 'name': s.product_name or product.name,
595 'default_code': s.product_code or product.default_code,
596 'variants': product.variants
598 result.append(_name_get(mydict))
602 'name': product.name,
603 'default_code': product.default_code,
604 'variants': product.variants
606 result.append(_name_get(mydict))
609 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
613 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
615 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
617 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
618 # on a database with thousands of matching products, due to the huge merge+unique needed for the
619 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
620 # Performing a quick memory merge of ids in Python will give much better performance
622 ids.update(self.search(cr, user, args + [('default_code',operator,name)], limit=limit, context=context))
624 # we may underrun the limit because of dupes in the results, that's fine
625 ids.update(self.search(cr, user, args + [('name',operator,name)], limit=(limit-len(ids)), context=context))
628 ptrn = re.compile('(\[(.*?)\])')
629 res = ptrn.search(name)
631 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
633 ids = self.search(cr, user, args, limit=limit, context=context)
634 result = self.name_get(cr, user, ids, context=context)
638 # Could be overrided for variants matrices prices
640 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
644 if 'currency_id' in context:
645 pricetype_obj = self.pool.get('product.price.type')
646 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
647 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
650 product_uom_obj = self.pool.get('product.uom')
651 for product in self.browse(cr, uid, ids, context=context):
652 res[product.id] = product[ptype] or 0.0
653 if ptype == 'list_price':
654 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
657 uom = product.uom_id or product.uos_id
658 res[product.id] = product_uom_obj._compute_price(cr, uid,
659 uom.id, res[product.id], context['uom'])
660 # Convert from price_type currency to asked one
661 if 'currency_id' in context:
662 # Take the price_type currency from the product field
663 # This is right cause a field cannot be in more than one currency
664 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
665 context['currency_id'], res[product.id],context=context)
669 def copy(self, cr, uid, id, default=None, context=None):
676 # Craft our own `<name> (copy)` in en_US (self.copy_translation()
677 # will do the other languages).
678 context_wo_lang = context.copy()
679 context_wo_lang.pop('lang', None)
680 product = self.read(cr, uid, id, ['name'], context=context_wo_lang)
681 default = default.copy()
682 default['name'] = product['name'] + ' (copy)'
684 if context.get('variant',False):
685 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
686 'price_margin', 'price_extra']
687 data = self.read(cr, uid, id, fields=fields, context=context)
691 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
692 and data['product_tmpl_id'][0]
694 return self.create(cr, uid, data)
696 return super(product_product, self).copy(cr, uid, id, default=default,
700 class product_packaging(osv.osv):
701 _name = "product.packaging"
702 _description = "Packaging"
706 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
707 'name' : fields.text('Description', size=64),
708 'qty' : fields.float('Quantity by Package',
709 help="The total number of products you can put by pallet or box."),
710 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
711 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
712 'rows' : fields.integer('Number of Layers', required=True,
713 help='The number of layers on a pallet or box'),
714 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
715 'ean' : fields.char('EAN', size=14,
716 help="The EAN code of the package unit."),
717 'code' : fields.char('Code', size=14,
718 help="The code of the transport unit."),
719 'weight': fields.float('Total Package Weight',
720 help='The weight of a full package, pallet or box.'),
721 'weight_ul': fields.float('Empty Package Weight',
722 help='The weight of the empty UL'),
723 'height': fields.float('Height', help='The height of the package'),
724 'width': fields.float('Width', help='The width of the package'),
725 'length': fields.float('Length', help='The length of the package'),
729 def _check_ean_key(self, cr, uid, ids, context=None):
730 for pack in self.browse(cr, uid, ids, context=context):
731 res = check_ean(pack.ean)
734 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
736 def name_get(self, cr, uid, ids, context=None):
740 for pckg in self.browse(cr, uid, ids, context=context):
741 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
742 p_name += pckg.ul.name
743 res.append((pckg.id,p_name))
746 def _get_1st_ul(self, cr, uid, context=None):
747 cr.execute('select id from product_ul order by id asc limit 1')
749 return (res and res[0]) or False
752 'rows' : lambda *a : 3,
753 'sequence' : lambda *a : 1,
758 salt = '31' * 6 + '3'
760 for ean_part, salt_part in zip(ean, salt):
761 sum += int(ean_part) * int(salt_part)
762 return (10 - (sum % 10)) % 10
763 checksum = staticmethod(checksum)
768 class product_supplierinfo(osv.osv):
769 _name = "product.supplierinfo"
770 _description = "Information about a product supplier"
771 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
773 product_uom_pool = self.pool.get('product.uom')
774 for supplier_info in self.browse(cr, uid, ids, context=context):
776 result[supplier_info.id] = {field:False}
777 qty = supplier_info.min_qty
778 result[supplier_info.id]['qty'] = qty
782 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
783 '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."),
784 '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."),
785 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
786 '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."),
787 '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."),
788 '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."),
789 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
790 '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."),
791 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
792 'company_id':fields.many2one('res.company','Company',select=1),
795 'qty': lambda *a: 0.0,
796 'sequence': lambda *a: 1,
797 'delay': lambda *a: 1,
798 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
800 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
802 Calculate price from supplier pricelist.
803 @param supplier_ids: Ids of res.partner object.
804 @param product_id: Id of product.
805 @param product_qty: specify quantity to purchase.
807 if type(supplier_ids) in (int,long,):
808 supplier_ids = [supplier_ids]
810 product_pool = self.pool.get('product.product')
811 partner_pool = self.pool.get('res.partner')
812 pricelist_pool = self.pool.get('product.pricelist')
813 currency_pool = self.pool.get('res.currency')
814 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
815 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
816 # Compute price from standard price of product
817 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
819 # Compute price from Purchase pricelist of supplier
820 pricelist_id = supplier.property_product_pricelist_purchase.id
822 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
823 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
825 # Compute price from supplier pricelist which are in Supplier Information
826 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
827 if supplier_info_ids:
828 cr.execute('SELECT * ' \
829 'FROM pricelist_partnerinfo ' \
830 'WHERE suppinfo_id IN %s' \
831 'AND min_quantity <= %s ' \
832 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
833 res2 = cr.dictfetchone()
835 price = res2['price']
836 res[supplier.id] = price
839 product_supplierinfo()
842 class pricelist_partnerinfo(osv.osv):
843 _name = 'pricelist.partnerinfo'
845 'name': fields.char('Description', size=64),
846 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
847 '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."),
848 '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"),
850 _order = 'min_quantity asc'
851 pricelist_partnerinfo()
852 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: