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 round(1.0 / factor, 6) 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)
97 'name': fields.char('Name', size=64, required=True, translate=True),
98 'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
99 help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
100 'factor': fields.float('Ratio', required=True,digits=(12, 12),
101 help='How many times this UoM is smaller than the reference UoM in this category:\n'\
102 '1 * (reference unit) = ratio * (this unit)'),
103 'factor_inv': fields.function(_factor_inv, digits_compute=dp.get_precision('Product UoM'),
104 fnct_inv=_factor_inv_write,
105 method=True, string='Ratio',
106 help='How many times this UoM is bigger than the reference UoM in this category:\n'\
107 '1 * (this unit) = ratio * (reference unit)', required=True),
108 'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product UoM'), required=True,
109 help="The computed quantity will be a multiple of this value. "\
110 "Use 1.0 for a UoM that cannot be further split, such as a piece."),
111 'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
112 'uom_type': fields.selection([('bigger','Bigger than the reference UoM'),
113 ('reference','Reference UoM for this category'),
114 ('smaller','Smaller than the reference UoM')],'UoM Type', required=1),
120 'uom_type': 'reference',
124 ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
127 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
128 if not from_uom_id or not qty or not to_uom_id:
130 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
131 if uoms[0].id == from_uom_id:
132 from_unit, to_unit = uoms[0], uoms[-1]
134 from_unit, to_unit = uoms[-1], uoms[0]
135 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
137 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
140 if from_unit.category_id.id <> to_unit.category_id.id:
141 if context.get('raise-exception', True):
142 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,))
145 amount = qty / from_unit.factor
147 amount = rounding(amount * to_unit.factor, to_unit.rounding)
150 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
151 if not from_uom_id or not price or not to_uom_id:
153 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
154 if uoms[0].id == from_uom_id:
155 from_unit, to_unit = uoms[0], uoms[-1]
157 from_unit, to_unit = uoms[-1], uoms[0]
158 if from_unit.category_id.id <> to_unit.category_id.id:
160 amount = price * from_unit.factor
162 amount = amount / to_unit.factor
165 def onchange_type(self, cursor, user, ids, value):
166 if value == 'reference':
167 return {'value': {'factor': 1, 'factor_inv': 1}}
173 class product_ul(osv.osv):
175 _description = "Shipping Unit"
177 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
178 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
183 #----------------------------------------------------------
185 #----------------------------------------------------------
186 class product_category(osv.osv):
188 def name_get(self, cr, uid, ids, context=None):
191 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
194 name = record['name']
195 if record['parent_id']:
196 name = record['parent_id'][1]+' / '+name
197 res.append((record['id'], name))
200 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
201 res = self.name_get(cr, uid, ids, context=context)
204 _name = "product.category"
205 _description = "Product Category"
207 'name': fields.char('Name', size=64, required=True, translate=True),
208 'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
209 'parent_id': fields.many2one('product.category','Parent Category', select=True),
210 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
211 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
212 'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
217 'type' : lambda *a : 'normal',
221 def _check_recursion(self, cr, uid, ids, context=None):
224 cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
225 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
232 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
234 def child_get(self, cr, uid, ids):
240 #----------------------------------------------------------
242 #----------------------------------------------------------
243 class product_template(osv.osv):
244 _name = "product.template"
245 _description = "Product Template"
246 def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
248 for product in self.browse(cr, uid, ids, context=context):
250 result[product.id] = {field:False}
251 result[product.id]['seller_delay'] = 1
252 if product.seller_ids:
253 partner_list = sorted([(partner_id.sequence, partner_id)
254 for partner_id in product.seller_ids
255 if partner_id and isinstance(partner_id.sequence, (int, long))])
256 main_supplier = partner_list and partner_list[0] and partner_list[0][1] or False
257 result[product.id]['seller_delay'] = main_supplier and main_supplier.delay or 1
258 result[product.id]['seller_qty'] = main_supplier and main_supplier.qty or 0.0
259 result[product.id]['seller_id'] = main_supplier and main_supplier.name.id or False
263 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
264 'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
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. Consumables are stockable products with infinite stock, or for use when you have no inventory management in the system."),
269 'supply_method': fields.selection([('produce','Produce'),('buy','Buy')], 'Supply method', required=True, help="Produce will generate production order or tasks, according to the product type. Purchase 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."),
277 'volume': fields.float('Volume', help="The volume in m3."),
278 'weight': fields.float('Gross weight', help="The gross weight in Kg."),
279 'weight_net': fields.float('Net 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 UOM.'),
294 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
295 help='Coefficient to convert UOM to UOS\n'
296 ' uos = uom * coeff'),
297 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
298 'seller_delay': fields.function(_calc_seller, method=True, type='integer', string='Supplier Lead Time', multi="seller_delay", 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."),
299 'seller_qty': fields.function(_calc_seller, method=True, type='float', string='Supplier Quantity', multi="seller_qty", help="This is minimum quantity to purchase from Main Supplier."),
300 'seller_id': fields.function(_calc_seller, method=True, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_id"),
301 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
302 'loc_rack': fields.char('Rack', size=16),
303 'loc_row': fields.char('Row', size=16),
304 'loc_case': fields.char('Case', size=16),
305 'company_id': fields.many2one('res.company', 'Company',select=1),
308 def _get_uom_id(self, cr, uid, *args):
309 cr.execute('select id from product_uom order by id limit 1')
311 return res and res[0] or False
313 def _default_category(self, cr, uid, context=None):
316 if 'categ_id' in context and context['categ_id']:
317 return context['categ_id']
318 md = self.pool.get('ir.model.data')
319 res = md.get_object_reference(cr, uid, 'product', 'cat0') or False
320 return res and res[1] or False
322 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
324 return {'value': {'uom_po_id': uom_id}}
328 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
329 'type': lambda *a: 'product',
330 'list_price': lambda *a: 1,
331 'cost_method': lambda *a: 'standard',
332 'supply_method': lambda *a: 'buy',
333 'standard_price': lambda *a: 1,
334 'sale_ok': lambda *a: 1,
335 'sale_delay': lambda *a: 7,
336 'produce_delay': lambda *a: 1,
337 'purchase_ok': lambda *a: 1,
338 'procure_method': lambda *a: 'make_to_stock',
339 'uom_id': _get_uom_id,
340 'uom_po_id': _get_uom_id,
341 'uos_coeff' : lambda *a: 1.0,
342 'mes_type' : lambda *a: 'fixed',
343 'categ_id' : _default_category,
344 'type' : lambda *a: 'consu',
347 def _check_uom(self, cursor, user, ids, context=None):
348 for product in self.browse(cursor, user, ids, context=context):
349 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
353 def _check_uos(self, cursor, user, ids, context=None):
354 for product in self.browse(cursor, user, ids, context=context):
356 and product.uos_id.category_id.id \
357 == product.uom_id.category_id.id:
362 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
365 def name_get(self, cr, user, ids, context=None):
368 if 'partner_id' in context:
370 return super(product_template, self).name_get(cr, user, ids, context)
374 class product_product(osv.osv):
375 def view_header_get(self, cr, uid, view_id, view_type, context=None):
378 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
379 if (context.get('categ_id', False)):
380 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
383 def _product_price(self, cr, uid, ids, name, arg, context=None):
387 quantity = context.get('quantity') or 1.0
388 pricelist = context.get('pricelist', False)
392 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
397 res.setdefault(id, 0.0)
400 def _get_product_available_func(states, what):
401 def _product_available(self, cr, uid, ids, name, arg, context=None):
402 return {}.fromkeys(ids, 0.0)
403 return _product_available
405 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
406 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
407 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
408 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
410 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
412 product_uom_obj = self.pool.get('product.uom')
414 res.setdefault(id, 0.0)
415 product_fields_to_read = ['uos_id', 'uom_id', 'list_price', 'price_margin', 'price_extra']
416 for product in self.read(cr, uid, ids, product_fields_to_read, context=context):
417 product_id = product['id']
419 uom_id = product['uos_id'] and product['uos_id'][0] or product['uom_id'][0]
420 res[product_id] = product_uom_obj._compute_price(cr, uid,
421 uom_id, product['list_price'], context['uom'])
423 res[product_id] = product['list_price']
424 res[product_id] = (res[product_id] or 0.0) * (product['price_margin'] or 1.0) + product['price_extra']
427 def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
428 for supinfo in product.seller_ids:
429 if supinfo.name.id == partner_id:
430 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
431 res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
434 def _product_code(self, cr, uid, ids, name, arg, context=None):
438 for p in self.browse(cr, uid, ids, context=context):
439 res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
442 def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
446 for p in self.browse(cr, uid, ids, context=context):
447 data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
448 if not data['variants']:
449 data['variants'] = p.variants
451 data['code'] = p.code
453 data['name'] = p.name
454 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
455 (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
459 'active': lambda *a: 1,
460 'price_extra': lambda *a: 0.0,
461 'price_margin': lambda *a: 1.0,
464 _name = "product.product"
465 _description = "Product"
466 _table = "product_product"
467 _inherits = {'product.template': 'product_tmpl_id'}
468 _order = 'default_code,name_template'
470 'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
471 'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
472 'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
473 'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
474 'price': fields.function(_product_price, method=True, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
475 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
476 'code': fields.function(_product_code, method=True, type='char', string='Reference'),
477 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
478 'default_code' : fields.char('Reference', size=64),
479 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
480 'variants': fields.char('Variants', size=64),
481 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
482 'ean13': fields.char('EAN13', size=13),
483 '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."),
484 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
485 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
486 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
487 'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True),
490 def unlink(self, cr, uid, ids, context=None):
492 unlink_product_tmpl_ids = []
493 for product in self.browse(cr, uid, ids, context=context):
494 tmpl_id = product.product_tmpl_id.id
495 # Check if the product is last product of this template
496 other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
497 if not other_product_ids:
498 unlink_product_tmpl_ids.append(tmpl_id)
499 unlink_ids.append(product.id)
500 self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
501 return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
503 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
504 if uom_id and uom_po_id:
505 uom_obj=self.pool.get('product.uom')
506 uom=uom_obj.browse(cursor,user,[uom_id])[0]
507 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
508 if uom.category_id.id != uom_po.category_id.id:
509 return {'value': {'uom_po_id': uom_id}}
512 def _check_ean_key(self, cr, uid, ids, context=None):
513 for product in self.browse(cr, uid, ids, context=context):
514 res = check_ean(product.ean13)
517 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
519 def on_order(self, cr, uid, ids, orderline, quantity):
522 def name_get(self, cr, user, ids, context=None):
528 name = d.get('name','')
529 code = d.get('default_code',False)
531 name = '[%s] %s' % (code,name)
532 if d.get('variants'):
533 name = name + ' - %s' % (d['variants'],)
534 return (d['id'], name)
536 partner_id = context.get('partner_id', False)
539 for product in self.browse(cr, user, ids, context=context):
540 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
545 'name': s.product_name or product.name,
546 'default_code': s.product_code or product.default_code,
547 'variants': product.variants
549 result.append(_name_get(mydict))
553 'name': product.name,
554 'default_code': product.default_code,
555 'variants': product.variants
557 result.append(_name_get(mydict))
560 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
564 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
566 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
568 ids = self.search(cr, user, ['|',('name',operator,name),('default_code',operator,name)] + args, limit=limit, context=context)
570 ptrn=re.compile('(\[(.*?)\])')
571 res = ptrn.search(name)
573 ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
575 ids = self.search(cr, user, args, limit=limit, context=context)
576 result = self.name_get(cr, user, ids, context=context)
580 # Could be overrided for variants matrices prices
582 def price_get(self, cr, uid, ids, ptype='list_price', context=None):
586 if 'currency_id' in context:
587 pricetype_obj = self.pool.get('product.price.type')
588 price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
589 price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
592 product_uom_obj = self.pool.get('product.uom')
593 for product in self.browse(cr, uid, ids, context=context):
594 res[product.id] = product[ptype] or 0.0
595 if ptype == 'list_price':
596 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
599 uom = product.uos_id or product.uom_id
600 res[product.id] = product_uom_obj._compute_price(cr, uid,
601 uom.id, res[product.id], context['uom'])
602 # Convert from price_type currency to asked one
603 if 'currency_id' in context:
604 # Take the price_type currency from the product field
605 # This is right cause a field cannot be in more than one currency
606 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
607 context['currency_id'], res[product.id],context=context)
611 def copy(self, cr, uid, id, default=None, context=None):
615 product = self.read(cr, uid, id, ['name'], context=context)
618 default = default.copy()
619 default['name'] = product['name'] + _(' (copy)')
621 if context.get('variant',False):
622 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
623 'price_margin', 'price_extra']
624 data = self.read(cr, uid, id, fields=fields, context=context)
628 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
629 and data['product_tmpl_id'][0]
631 return self.create(cr, uid, data)
633 return super(product_product, self).copy(cr, uid, id, default=default,
637 class product_packaging(osv.osv):
638 _name = "product.packaging"
639 _description = "Packaging"
643 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
644 'name' : fields.text('Description', size=64),
645 'qty' : fields.float('Quantity by Package',
646 help="The total number of products you can put by pallet or box."),
647 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
648 'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
649 'rows' : fields.integer('Number of Layers', required=True,
650 help='The number of layers on a pallet or box'),
651 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
652 'ean' : fields.char('EAN', size=14,
653 help="The EAN code of the package unit."),
654 'code' : fields.char('Code', size=14,
655 help="The code of the transport unit."),
656 'weight': fields.float('Total Package Weight',
657 help='The weight of a full package, pallet or box.'),
658 'weight_ul': fields.float('Empty Package Weight',
659 help='The weight of the empty UL'),
660 'height': fields.float('Height', help='The height of the package'),
661 'width': fields.float('Width', help='The width of the package'),
662 'length': fields.float('Length', help='The length of the package'),
666 def _check_ean_key(self, cr, uid, ids, context=None):
667 for pack in self.browse(cr, uid, ids, context=context):
668 res = check_ean(pack.ean)
671 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
673 def name_get(self, cr, uid, ids, context=None):
677 for pckg in self.browse(cr, uid, ids, context=context):
678 p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
679 p_name += pckg.ul.name
680 res.append((pckg.id,p_name))
683 def _get_1st_ul(self, cr, uid, context=None):
684 cr.execute('select id from product_ul order by id asc limit 1')
686 return (res and res[0]) or False
689 'rows' : lambda *a : 3,
690 'sequence' : lambda *a : 1,
695 salt = '31' * 6 + '3'
697 for ean_part, salt_part in zip(ean, salt):
698 sum += int(ean_part) * int(salt_part)
699 return (10 - (sum % 10)) % 10
700 checksum = staticmethod(checksum)
705 class product_supplierinfo(osv.osv):
706 _name = "product.supplierinfo"
707 _description = "Information about a product supplier"
708 def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
710 product_uom_pool = self.pool.get('product.uom')
711 for supplier_info in self.browse(cr, uid, ids, context=context):
713 result[supplier_info.id] = {field:False}
714 if supplier_info.product_uom.id:
715 qty = product_uom_pool._compute_qty(cr, uid, supplier_info.product_uom.id, supplier_info.min_qty, to_uom_id=supplier_info.product_id.uom_id.id)
717 qty = supplier_info.min_qty
718 result[supplier_info.id]['qty'] = qty
721 def _get_uom_id(self, cr, uid, *args):
722 cr.execute('select id from product_uom order by id limit 1')
724 return res and res[0] or False
727 'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
728 '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."),
729 '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."),
730 'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
731 'product_uom': fields.many2one('product.uom', string="Supplier UoM", help="Choose here the Unit of Measure in which the prices and quantities are expressed below."),
732 'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product UoM if not empty, in the default unit of measure of the product otherwise."),
733 'qty': fields.function(_calc_qty, method=True, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Uom."),
734 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
735 '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."),
736 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
737 'company_id':fields.many2one('res.company','Company',select=1),
740 'qty': lambda *a: 0.0,
741 'sequence': lambda *a: 1,
742 'delay': lambda *a: 1,
743 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
744 'product_uom': _get_uom_id,
746 def _check_uom(self, cr, uid, ids, context=None):
747 for supplier_info in self.browse(cr, uid, ids, context=context):
748 if supplier_info.product_uom and supplier_info.product_uom.category_id.id <> supplier_info.product_id.uom_id.category_id.id:
753 (_check_uom, 'Error: The default UOM and the Supplier Product UOM must be in the same category.', ['product_uom']),
755 def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
757 Calculate price from supplier pricelist.
758 @param supplier_ids: Ids of res.partner object.
759 @param product_id: Id of product.
760 @param product_qty: specify quantity to purchase.
762 if type(supplier_ids) in (int,long,):
763 supplier_ids = [supplier_ids]
765 product_pool = self.pool.get('product.product')
766 partner_pool = self.pool.get('res.partner')
767 pricelist_pool = self.pool.get('product.pricelist')
768 currency_pool = self.pool.get('res.currency')
769 currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
770 for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
771 # Compute price from standard price of product
772 price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
774 # Compute price from Purchase pricelist of supplier
775 pricelist_id = supplier.property_product_pricelist_purchase.id
777 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
778 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
780 # Compute price from supplier pricelist which are in Supplier Information
781 supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
782 if supplier_info_ids:
783 cr.execute('SELECT * ' \
784 'FROM pricelist_partnerinfo ' \
785 'WHERE suppinfo_id IN %s' \
786 'AND min_quantity <= %s ' \
787 'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
788 res2 = cr.dictfetchone()
790 price = res2['price']
791 res[supplier.id] = price
794 product_supplierinfo()
797 class pricelist_partnerinfo(osv.osv):
798 _name = 'pricelist.partnerinfo'
800 'name': fields.char('Description', size=64),
801 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
802 'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier UoM if any or in the default UoM of the product otherrwise."),
803 '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 UoM if any or the default Unit of Measure of the product otherwise"),
805 _order = 'min_quantity asc'
806 pricelist_partnerinfo()
807 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: