1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from osv import osv, fields
27 from _common import rounding
29 from tools import config
30 from tools.translate import _
35 #----------------------------------------------------------
37 #----------------------------------------------------------
39 class product_uom_categ(osv.osv):
40 _name = 'product.uom.categ'
41 _description = 'Product uom categ'
43 'name': fields.char('Name', size=64, required=True, translate=True),
47 class product_uom(osv.osv):
49 _description = 'Product Unit of Measure'
51 def _factor(self, cursor, user, ids, name, arg, context):
53 for uom in self.browse(cursor, user, ids, context=context):
55 if uom.factor_inv_data:
56 res[uom.id] = uom.factor_inv_data
58 res[uom.id] = round(1 / uom.factor, 6)
63 def _factor_inv(self, cursor, user, id, name, value, arg, context):
65 if 'read_delta' in ctx:
69 if round(1 / round(1/value, 6), 6) != value:
71 self.write(cursor, user, id, {
72 'factor': round(1/value, 6),
73 'factor_inv_data': data,
76 self.write(cursor, user, id, {
78 'factor_inv_data': 0.0,
82 'name': fields.char('Name', size=64, required=True, translate=True),
83 'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
84 help="Unit of Measure of a category can be converted between each others in the same category."),
85 'factor': fields.float('Rate', digits=(12, 6), required=True,
86 help='The coefficient for the formula:\n' \
87 '1 (base unit) = coeff (this unit). Rate = 1 / Factor.'),
88 'factor_inv': fields.function(_factor, digits=(12, 6),
89 method=True, string='Factor',
90 help='The coefficient for the formula:\n' \
91 'coeff (base unit) = 1 (this unit). Factor = 1 / Rate.'),
92 'factor_inv_data': fields.float('Factor', digits=(12, 6)),
93 'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
94 help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be split."),
95 'active': fields.boolean('Active'),
99 'factor': lambda *a: 1.0,
100 'factor_inv': lambda *a: 1.0,
101 'active': lambda *a: 1,
102 'rounding': lambda *a: 0.01,
105 def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
106 if not from_uom_id or not qty or not to_uom_id:
108 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
109 if uoms[0].id == from_uom_id:
110 from_unit, to_unit = uoms[0], uoms[-1]
112 from_unit, to_unit = uoms[-1], uoms[0]
113 return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
115 def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context={}):
116 if from_unit.category_id.id <> to_unit.category_id.id:
117 raise osv.except_osv(_('Warning !'),_('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))
119 if from_unit.factor_inv_data:
120 amount = qty * from_unit.factor_inv_data
122 amount = qty / from_unit.factor
124 if to_unit.factor_inv_data:
125 amount = rounding(amount / to_unit.factor_inv_data, to_unit.rounding)
127 amount = rounding(amount * to_unit.factor, to_unit.rounding)
130 def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
131 if not from_uom_id or not price or not to_uom_id:
133 uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
134 if uoms[0].id == from_uom_id:
135 from_unit, to_unit = uoms[0], uoms[-1]
137 from_unit, to_unit = uoms[-1], uoms[0]
138 if from_unit.category_id.id <> to_unit.category_id.id:
139 raise osv.except_osv(_('Warning !'),_('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))
141 if from_unit.factor_inv_data:
142 amount = price / from_unit.factor_inv_data
144 amount = price * from_unit.factor
146 if to_unit.factor_inv_data:
147 amount = amount * to_unit.factor_inv_data
149 amount = amount / to_unit.factor
152 def onchange_factor_inv(self, cursor, user, ids, value):
154 return {'value': {'factor': 0}}
155 return {'value': {'factor': round(1/value, 6)}}
157 def onchange_factor(self, cursor, user, ids, value):
159 return {'value': {'factor_inv': 0}}
160 return {'value': {'factor_inv': round(1/value, 6)}}
165 class product_ul(osv.osv):
167 _description = "Shipping Unit"
169 'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
170 'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Pallet')], 'Type', required=True),
175 #----------------------------------------------------------
177 #----------------------------------------------------------
178 class product_category(osv.osv):
180 def name_get(self, cr, uid, ids, context=None):
183 reads = self.read(cr, uid, ids, ['name','parent_id'], context)
186 name = record['name']
187 if record['parent_id']:
188 name = record['parent_id'][1]+' / '+name
189 res.append((record['id'], name))
192 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
193 res = self.name_get(cr, uid, ids, context)
196 _name = "product.category"
197 _description = "Product Category"
199 'name': fields.char('Name', size=64, required=True, translate=True),
200 'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
201 'parent_id': fields.many2one('product.category','Parent Category', select=True),
202 'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
203 'sequence': fields.integer('Sequence'),
206 def _check_recursion(self, cr, uid, ids):
209 cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str,ids))+')')
210 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
217 (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
219 def child_get(self, cr, uid, ids):
225 #----------------------------------------------------------
227 #----------------------------------------------------------
228 class product_template(osv.osv):
229 _name = "product.template"
230 _description = "Product Template"
231 def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
233 for product in self.browse(cr, uid, ids, context):
234 if product.seller_ids:
235 result[product.id] = product.seller_ids[0].delay
237 result[product.id] = 1
241 'name': fields.char('Name', size=128, required=True, translate=True, select=True),
242 'product_manager': fields.many2one('res.users','Product Manager'),
243 'description': fields.text('Description',translate=True),
244 'description_purchase': fields.text('Purchase Description',translate=True),
245 'description_sale': fields.text('Sale Description',translate=True),
246 '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 stock management in the system."),
247 '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."),
248 'sale_delay': fields.float('Customer Lead Time', help="This is the average time between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
249 'produce_delay': fields.float('Manufacturing Lead Time', help="Average time 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 delays will be summed for all levels and purchase orders."),
250 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procure 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."),
251 'rental': fields.boolean('Rentable Product'),
252 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
253 'list_price': fields.float('Sale Price', digits=(16, int(config['price_accuracy'])), help="Base price for computing the customer price. Sometimes called the catalog price."),
254 'standard_price': fields.float('Cost Price', required=True, digits=(16, int(config['price_accuracy'])), help="The cost of the product for accounting stock valuation. It can serves as a base price for supplier price."),
255 'volume': fields.float('Volume', help="The volume in m3."),
256 'weight': fields.float('Gross weight', help="The gross weight in Kg."),
257 'weight_net': fields.float('Net weight', help="The net weight in Kg."),
258 'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
259 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."),
260 'warranty': fields.float('Warranty (months)'),
261 'sale_ok': fields.boolean('Can be sold', help="Determine if the product can be visible in the list of product within a selection from a sale order line."),
262 '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."),
263 'state': fields.selection([('',''),('draft', 'In Development'),('sellable','In Production'),('end','End of Lifecycle'),('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
264 'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
265 'uom_po_id': fields.many2one('product.uom', 'Purchase UoM', required=True, help="Default Unit of Measure used for purchase orders. It must in the same category than the default unit of measure."),
266 'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
267 help='Used by companies that manages two unit of measure: invoicing and stock management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
268 'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
269 help='Coefficient to convert UOM to UOS\n'
270 ' uos = uom * coeff'),
271 'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
272 'seller_delay': fields.function(_calc_seller_delay, method=True, type='integer', string='Supplier Lead Time', 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."),
273 'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
274 'loc_rack': fields.char('Rack', size=16),
275 'loc_row': fields.char('Row', size=16),
276 'loc_case': fields.char('Case', size=16),
277 'company_id': fields.many2one('res.company', 'Company'),
280 def _get_uom_id(self, cr, uid, *args):
281 cr.execute('select id from product_uom order by id limit 1')
283 return res and res[0] or False
285 def _default_category(self, cr, uid, context={}):
286 if 'categ_id' in context and context['categ_id']:
287 return context['categ_id']
290 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
291 if uom_id and uom_po_id:
292 uom_obj=self.pool.get('product.uom')
293 uom=uom_obj.browse(cursor,user,[uom_id])[0]
294 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
295 if uom.category_id.id != uom_po.category_id.id:
296 return {'value': {'uom_po_id': uom_id}}
300 'company_id': lambda self, cr, uid, context: False, # Visible by all
301 'type': lambda *a: 'product',
302 'list_price': lambda *a: 1,
303 'cost_method': lambda *a: 'standard',
304 'supply_method': lambda *a: 'buy',
305 'standard_price': lambda *a: 1,
306 'sale_ok': lambda *a: 1,
307 'sale_delay': lambda *a: 7,
308 'produce_delay': lambda *a: 1,
309 'purchase_ok': lambda *a: 1,
310 'procure_method': lambda *a: 'make_to_stock',
311 'uom_id': _get_uom_id,
312 'uom_po_id': _get_uom_id,
313 'uos_coeff' : lambda *a: 1.0,
314 'mes_type' : lambda *a: 'fixed',
315 'categ_id' : _default_category,
318 def _check_uom(self, cursor, user, ids):
319 for product in self.browse(cursor, user, ids):
320 if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
324 def _check_uos(self, cursor, user, ids):
325 for product in self.browse(cursor, user, ids):
327 and product.uos_id.category_id.id \
328 == product.uom_id.category_id.id:
333 (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
334 (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
337 def name_get(self, cr, user, ids, context={}):
338 if 'partner_id' in context:
340 return super(product_template, self).name_get(cr, user, ids, context)
344 class product_product(osv.osv):
345 def view_header_get(self, cr, uid, view_id, view_type, context):
346 res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
347 if (context.get('categ_id', False)):
348 return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
351 def _product_price(self, cr, uid, ids, name, arg, context={}):
353 quantity = context.get('quantity', 1)
354 pricelist = context.get('pricelist', False)
358 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
363 res.setdefault(id, 0.0)
366 def _get_product_available_func(states, what):
367 def _product_available(self, cr, uid, ids, name, arg, context={}):
368 return {}.fromkeys(ids, 0.0)
369 return _product_available
371 _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
372 _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
373 _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
374 _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
376 def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
378 product_uom_obj = self.pool.get('product.uom')
380 res.setdefault(id, 0.0)
381 for product in self.browse(cr, uid, ids, context=context):
383 uom = product.uos_id or product.uom_id
384 res[product.id] = product_uom_obj._compute_price(cr, uid,
385 uom.id, product.list_price, context['uom'])
387 res[product.id] = product.list_price
388 res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
391 def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
392 product = self.browse(cr, uid, [product_id], context)[0]
393 for supinfo in product.seller_ids:
394 if supinfo.name.id == partner_id:
395 return {'code': supinfo.product_code, 'name': supinfo.product_name}
396 return {'code' : product.default_code, 'name' : product.name}
398 def _product_code(self, cr, uid, ids, name, arg, context={}):
400 for p in self.browse(cr, uid, ids, context):
401 res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
404 def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
406 for p in self.browse(cr, uid, ids, context):
407 data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
409 data['code'] = p.code
411 data['name'] = p.name
412 res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
417 'active': lambda *a: 1,
418 'price_extra': lambda *a: 0.0,
419 'price_margin': lambda *a: 1.0,
422 _name = "product.product"
423 _description = "Product"
424 _table = "product_product"
425 _inherits = {'product.template': 'product_tmpl_id'}
427 'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
428 'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
429 'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
430 'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
431 'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
432 'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
433 'code': fields.function(_product_code, method=True, type='char', string='Code'),
434 'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
435 'default_code' : fields.char('Code', size=64),
436 'active': fields.boolean('Active'),
437 'variants': fields.char('Variants', size=64),
438 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
439 'ean13': fields.char('EAN13', size=13),
440 '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 packing order and is mainly used if you use the EDI module."),
441 'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
442 'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
445 def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
446 if uom_id and uom_po_id:
447 uom_obj=self.pool.get('product.uom')
448 uom=uom_obj.browse(cursor,user,[uom_id])[0]
449 uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
450 if uom.category_id.id != uom_po.category_id.id:
451 return {'value': {'uom_po_id': uom_id}}
454 def _check_ean_key(self, cr, uid, ids):
455 for partner in self.browse(cr, uid, ids):
456 if not partner.ean13:
458 if len(partner.ean13) <> 13:
467 sum += int(partner.ean13[i])
469 sum += 3 * int(partner.ean13[i])
470 check = int(math.ceil(sum / 10.0) * 10 - sum)
471 if check != int(partner.ean13[12]):
475 _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
477 def on_order(self, cr, uid, ids, orderline, quantity):
480 def name_get(self, cr, user, ids, context={}):
484 #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
485 #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
486 name = d.get('name','')
487 code = d.get('default_code',False)
489 name = '[%s] %s' % (code,name)
491 name = name + ' - %s' % (d['variants'],)
492 return (d['id'], name)
493 result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
496 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
502 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
504 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
506 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
507 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
509 ids = self.search(cr, user, args, limit=limit, context=context)
510 result = self.name_get(cr, user, ids, context)
514 # Could be overrided for variants matrices prices
516 def price_get(self, cr, uid, ids, ptype='list_price', context={}):
518 product_uom_obj = self.pool.get('product.uom')
520 for product in self.browse(cr, uid, ids, context=context):
521 res[product.id] = product[ptype] or 0.0
522 if ptype == 'list_price':
523 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
526 uom = product.uos_id or product.uom_id
527 res[product.id] = product_uom_obj._compute_price(cr, uid,
528 uom.id, res[product.id], context['uom'])
531 def copy(self, cr, uid, id, default=None, context=None):
535 if ('variant' in context) and context['variant']:
536 fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
537 'price_margin', 'price_extra']
538 data = self.read(cr, uid, id, fields=fields, context=context)
542 data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
543 and data['product_tmpl_id'][0]
545 return self.create(cr, uid, data)
547 return super(product_product, self).copy(cr, uid, id, default=default,
551 class product_packaging(osv.osv):
552 _name = "product.packaging"
553 _description = "Packaging"
556 'sequence': fields.integer('Sequence'),
557 'name' : fields.char('Description', size=64),
558 'qty' : fields.float('Quantity by Package',
559 help="The total number of products you can put by palet or box."),
560 'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
561 'ul_qty' : fields.integer('Package by layer'),
562 'rows' : fields.integer('Number of Layer', required=True,
563 help='The number of layer on a palet or box'),
564 'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
565 'ean' : fields.char('EAN', size=14,
566 help="The EAN code of the package unit."),
567 'code' : fields.char('Code', size=14,
568 help="The code of the transport unit."),
569 'weight': fields.float('Total Package Weight',
570 help='The weight of a full of products palet or box.'),
571 'weight_ul': fields.float('Empty Package Weight',
572 help='The weight of the empty UL'),
573 'height': fields.float('Height', help='The height of the package'),
574 'width': fields.float('Width', help='The width of the package'),
575 'length': fields.float('Length', help='The length of the package'),
578 def _get_1st_ul(self, cr, uid, context={}):
579 cr.execute('select id from product_ul order by id asc limit 1')
581 return (res and res[0]) or False
584 'rows' : lambda *a : 3,
585 'sequence' : lambda *a : 1,
590 salt = '31' * 6 + '3'
592 for ean_part, salt_part in zip(ean, salt):
593 sum += int(ean_part) * int(salt_part)
594 return (10 - (sum % 10)) % 10
595 checksum = staticmethod(checksum)
600 class product_supplierinfo(osv.osv):
601 _name = "product.supplierinfo"
602 _description = "Information about a product supplier"
604 'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
605 'product_name': fields.char('Partner Product Name', size=128, help="Name of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
606 'product_code': fields.char('Partner Product Code', size=64, help="Code of the product for this partner, will be used when printing a request for quotation. Keep empty to use the internal one."),
607 'sequence' : fields.integer('Priority'),
608 'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the default unit of measure."),
609 'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
610 'delay' : fields.integer('Delivery Delay', required=True, help="Delay 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."),
611 'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
614 'qty': lambda *a: 0.0,
615 'sequence': lambda *a: 1,
616 'delay': lambda *a: 1,
619 product_supplierinfo()
622 class pricelist_partnerinfo(osv.osv):
623 _name = 'pricelist.partnerinfo'
625 'name': fields.char('Description', size=64),
626 'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
627 'min_quantity': fields.float('Quantity', required=True),
628 'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
630 _order = 'min_quantity asc'
631 pricelist_partnerinfo()
635 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: