merge
[odoo/odoo.git] / addons / product / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from osv import osv, fields
23 import pooler
24
25 import math
26 from _common import rounding
27
28 from tools import config
29 from tools.translate import _
30
31 def is_pair(x):
32     return not x%2
33
34 #----------------------------------------------------------
35 # UOM
36 #----------------------------------------------------------
37
38 class product_uom_categ(osv.osv):
39     _name = 'product.uom.categ'
40     _description = 'Product uom categ'
41     _columns = {
42         'name': fields.char('Name', size=64, required=True, translate=True),
43     }
44 product_uom_categ()
45
46 class product_uom(osv.osv):
47     _name = 'product.uom'
48     _description = 'Product Unit of Measure'
49
50     def _factor(self, cursor, user, ids, name, arg, context):
51         res = {}
52         for uom in self.browse(cursor, user, ids, context=context):
53             if uom.factor:
54                 if uom.factor_inv_data:
55                     res[uom.id] = uom.factor_inv_data
56                 else:
57                     res[uom.id] = round(1 / uom.factor, 6)
58             else:
59                 res[uom.id] = 0.0
60         return res
61
62     def _factor_inv(self, cursor, user, id, name, value, arg, context):
63         ctx = context.copy()
64         if 'read_delta' in ctx:
65             del ctx['read_delta']
66         if value:
67             data = 0.0
68             if round(1 / round(1/value, 6), 6) != value:
69                 data = value
70             self.write(cursor, user, id, {
71                 'factor': round(1/value, 6),
72                 'factor_inv_data': data,
73                 }, context=ctx)
74         else:
75             self.write(cursor, user, id, {
76                 'factor': 0.0,
77                 'factor_inv_data': 0.0,
78                 }, context=ctx)
79
80     _columns = {
81         'name': fields.char('Name', size=64, required=True, translate=True),
82         'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
83             help="Unit of Measure of a category can be converted between each others in the same category."),
84         'factor': fields.float('Rate', digits=(12, 6), required=True,
85             help='The coefficient for the formula:\n' \
86                     '1 (base unit) = coeff (this unit). Rate = 1 / Factor.'),
87         'factor_inv': fields.function(_factor, digits=(12, 6),
88             method=True, string='Factor',
89             help='The coefficient for the formula:\n' \
90                     'coeff (base unit) = 1 (this unit). Factor = 1 / Rate.'),
91         'factor_inv_data': fields.float('Factor', digits=(12, 6)),
92         'rounding': fields.float('Rounding Precision', digits=(16, 3), required=True,
93             help="The computed quantity will be a multiple of this value. Use 1.0 for products that can not be split."),
94         'active': fields.boolean('Active'),
95     }
96
97     _defaults = {
98         'factor': lambda *a: 1.0,
99         'factor_inv': lambda *a: 1.0,
100         'active': lambda *a: 1,
101         'rounding': lambda *a: 0.01,
102     }
103
104     _sql_constraints = [
105         ('factor_gt_zero', 'CHECK (factor!=0)', 'Value of the factor can never be 0 !'),
106         ('factor_inv_data_gt_zero', 'CHECK (factor_inv_data!=0)', 'Value of the factor_inv_data can never be 0 !'),
107     ]
108
109     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
110         if not from_uom_id or not qty or not to_uom_id:
111             return qty
112         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
113         if uoms[0].id == from_uom_id:
114             from_unit, to_unit = uoms[0], uoms[-1]
115         else:
116             from_unit, to_unit = uoms[-1], uoms[0]
117         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
118
119     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context={}):
120         if from_unit.category_id.id <> to_unit.category_id.id:
121             return qty
122         if from_unit.factor_inv_data:
123             amount = qty * from_unit.factor_inv_data
124         else:
125             amount = qty / from_unit.factor
126         if to_unit:
127             if to_unit.factor_inv_data:
128                 amount = rounding(amount / to_unit.factor_inv_data, to_unit.rounding)
129             else:
130                 amount = rounding(amount * to_unit.factor, to_unit.rounding)
131         return amount
132
133     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
134         if not from_uom_id or not price or not to_uom_id:
135             return price
136         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
137         if uoms[0].id == from_uom_id:
138             from_unit, to_unit = uoms[0], uoms[-1]
139         else:
140             from_unit, to_unit = uoms[-1], uoms[0]
141         if from_unit.category_id.id <> to_unit.category_id.id:
142             return price
143         if from_unit.factor_inv_data:
144             amount = price / from_unit.factor_inv_data
145         else:
146             amount = price * from_unit.factor
147         if to_uom_id:
148             if to_unit.factor_inv_data:
149                 amount = amount * to_unit.factor_inv_data
150             else:
151                 amount = amount / to_unit.factor
152         return amount
153
154     def onchange_factor_inv(self, cursor, user, ids, value):
155         if value == 0.0:
156             return {'value': {'factor': 0}}
157         return {'value': {'factor': round(1/value, 6)}}
158
159     def onchange_factor(self, cursor, user, ids, value):
160         if value == 0.0:
161             return {'value': {'factor_inv': 0}}
162         return {'value': {'factor_inv': round(1/value, 6)}}
163
164 product_uom()
165
166
167 class product_ul(osv.osv):
168     _name = "product.ul"
169     _description = "Shipping Unit"
170     _columns = {
171         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
172         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('palet', 'Pallet')], 'Type', required=True),
173     }
174 product_ul()
175
176
177 #----------------------------------------------------------
178 # Categories
179 #----------------------------------------------------------
180 class product_category(osv.osv):
181
182     def name_get(self, cr, uid, ids, context=None):
183         if not len(ids):
184             return []
185         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
186         res = []
187         for record in reads:
188             name = record['name']
189             if record['parent_id']:
190                 name = record['parent_id'][1]+' / '+name
191             res.append((record['id'], name))
192         return res
193
194     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context):
195         res = self.name_get(cr, uid, ids, context)
196         return dict(res)
197
198     _name = "product.category"
199     _description = "Product Category"
200     _columns = {
201         'name': fields.char('Name', size=64, required=True, translate=True),
202         'complete_name': fields.function(_name_get_fnc, method=True, type="char", string='Name'),
203         'parent_id': fields.many2one('product.category','Parent Category', select=True),
204         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
205         'sequence': fields.integer('Sequence'),
206     }
207     _order = "sequence"
208     def _check_recursion(self, cr, uid, ids):
209         level = 100
210         while len(ids):
211             cr.execute('select distinct parent_id from product_category where id in ('+','.join(map(str, ids))+')')
212             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
213             if not level:
214                 return False
215             level -= 1
216         return True
217
218     _constraints = [
219         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
220     ]
221     def child_get(self, cr, uid, ids):
222         return [ids]
223
224 product_category()
225
226
227 #----------------------------------------------------------
228 # Products
229 #----------------------------------------------------------
230 class product_template(osv.osv):
231     _name = "product.template"
232     _description = "Product Template"
233     def _calc_seller_delay(self, cr, uid, ids, name, arg, context={}):
234         result = {}
235         for product in self.browse(cr, uid, ids, context):
236             if product.seller_ids:
237                 result[product.id] = product.seller_ids[0].delay
238             else:
239                 result[product.id] = 1
240         return result
241
242     _columns = {
243         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
244         'product_manager': fields.many2one('res.users','Product Manager'),
245         'description': fields.text('Description',translate=True),
246         'description_purchase': fields.text('Purchase Description',translate=True),
247         'description_sale': fields.text('Sale Description',translate=True),
248         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way requisitions are processed. Consumables are stockable products with infinite stock, or for use when you have no inventory management in the system."),
249         '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."),
250         '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."),
251         '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."),
252         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Requisition 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 requisition request."),
253         'rental': fields.boolean('Can be Rent'),
254         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True),
255         '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."),
256         '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."),
257         'volume': fields.float('Volume', help="The volume in m3."),
258         'weight': fields.float('Gross weight', help="The gross weight in Kg."),
259         'weight_net': fields.float('Net weight', help="The net weight in Kg."),
260         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
261             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."),
262         'warranty': fields.float('Warranty (months)'),
263         '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."),
264         '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."),
265         'state': fields.selection([('',''),('draft', 'In Development'),('sellable','In Production'),('end','End of Lifecycle'),('obsolete','Obsolete')], 'State', help="Tells the user if he can use the product or not."),
266         'uom_id': fields.many2one('product.uom', 'Default UoM', required=True, help="Default Unit of Measure used for all stock operation."),
267         '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."),
268         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
269             help='Used by companies that manages two unit 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.'),
270         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
271             help='Coefficient to convert UOM to UOS\n'
272             ' uom = uos * coeff'),
273         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
274         '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."),
275         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
276         'loc_rack': fields.char('Rack', size=16),
277         'loc_row': fields.char('Row', size=16),
278         'loc_case': fields.char('Case', size=16),
279         'company_id': fields.many2one('res.company', 'Company'),
280     }
281
282     def _get_uom_id(self, cr, uid, *args):
283         cr.execute('select id from product_uom order by id limit 1')
284         res = cr.fetchone()
285         return res and res[0] or False
286
287     def _default_category(self, cr, uid, context={}):
288         if 'categ_id' in context and context['categ_id']:
289             return context['categ_id']
290         return False
291
292     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
293         if uom_id and uom_po_id:
294             uom_obj=self.pool.get('product.uom')
295             uom=uom_obj.browse(cursor,user,[uom_id])[0]
296             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
297             if uom.category_id.id != uom_po.category_id.id:
298                 return {'value': {'uom_po_id': uom_id}}
299         return False
300
301     _defaults = {
302         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', c),
303 #        'company_id': lambda self, cr, uid, context: False, # Visible by all
304         'type': lambda *a: 'product',
305         'list_price': lambda *a: 1,
306         'cost_method': lambda *a: 'standard',
307         'supply_method': lambda *a: 'buy',
308         'standard_price': lambda *a: 1,
309         'sale_ok': lambda *a: 1,
310         'sale_delay': lambda *a: 7,
311         'produce_delay': lambda *a: 1,
312         'purchase_ok': lambda *a: 1,
313         'procure_method': lambda *a: 'make_to_stock',
314         'uom_id': _get_uom_id,
315         'uom_po_id': _get_uom_id,
316         'uos_coeff' : lambda *a: 1.0,
317         'mes_type' : lambda *a: 'fixed',
318         'categ_id' : _default_category,
319     }
320
321     def _check_uom(self, cursor, user, ids):
322         for product in self.browse(cursor, user, ids):
323             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
324                 return False
325         return True
326
327     def _check_uos(self, cursor, user, ids):
328         for product in self.browse(cursor, user, ids):
329             if product.uos_id \
330                     and product.uos_id.category_id.id \
331                     == product.uom_id.category_id.id:
332                 return False
333         return True
334
335     _constraints = [
336         (_check_uos, 'Error: UOS must be in a different category than the UOM', ['uos_id']),
337         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
338     ]
339
340     def name_get(self, cr, user, ids, context={}):
341         if 'partner_id' in context:
342             pass
343         return super(product_template, self).name_get(cr, user, ids, context)
344
345 product_template()
346
347 class product_product(osv.osv):
348     def view_header_get(self, cr, uid, view_id, view_type, context):
349         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
350         if (context.get('categ_id', False)):
351             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context).name
352         return res
353
354     def _product_price(self, cr, uid, ids, name, arg, context={}):
355         res = {}
356         quantity = context.get('quantity', 1)
357         pricelist = context.get('pricelist', False)
358         if pricelist:
359             for id in ids:
360                 try:
361                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
362                 except:
363                     price = 0.0
364                 res[id] = price
365         for id in ids:
366             res.setdefault(id, 0.0)
367         return res
368
369     def _get_product_available_func(states, what):
370         def _product_available(self, cr, uid, ids, name, arg, context={}):
371             return {}.fromkeys(ids, 0.0)
372         return _product_available
373
374     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
375     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
376     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
377     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
378
379     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
380         res = {}
381         product_uom_obj = self.pool.get('product.uom')
382         for id in ids:
383             res.setdefault(id, 0.0)
384         for product in self.browse(cr, uid, ids, context=context):
385             if 'uom' in context:
386                 uom = product.uos_id or product.uom_id
387                 res[product.id] = product_uom_obj._compute_price(cr, uid,
388                         uom.id, product.list_price, context['uom'])
389             else:
390                 res[product.id] = product.list_price
391             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
392         return res
393
394     def _get_partner_code_name(self, cr, uid, ids, product_id, partner_id, context={}):
395         product = self.browse(cr, uid, [product_id], context)[0]
396         for supinfo in product.seller_ids:
397             if supinfo.name.id == partner_id:
398                 return {'code': supinfo.product_code, 'name': supinfo.product_name, 'variants': ''}
399         return {'code' : product.default_code, 'name' : product.name, 'variants': product.variants}
400
401     def _product_code(self, cr, uid, ids, name, arg, context={}):
402         res = {}
403         for p in self.browse(cr, uid, ids, context):
404             res[p.id] = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)['code']
405         return res
406
407     def _product_partner_ref(self, cr, uid, ids, name, arg, context={}):
408         res = {}
409         for p in self.browse(cr, uid, ids, context):
410             data = self._get_partner_code_name(cr, uid, [], p.id, context.get('partner_id', None), context)
411             if not data['variants']:
412                 data['variants'] = p.variants
413             if not data['code']:
414                 data['code'] = p.code
415             if not data['name']:
416                 data['name'] = p.name
417             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
418                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
419         return res
420
421     _defaults = {
422         'active': lambda *a: 1,
423         'price_extra': lambda *a: 0.0,
424         'price_margin': lambda *a: 1.0,
425     }
426
427     _name = "product.product"
428     _description = "Product"
429     _table = "product_product"
430     _inherits = {'product.template': 'product_tmpl_id'}
431     _columns = {
432         'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
433         'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
434         'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
435         'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
436         'price': fields.function(_product_price, method=True, type='float', string='Customer Price', digits=(16, int(config['price_accuracy']))),
437         'lst_price' : fields.function(_product_lst_price, method=True, type='float', string='List Price', digits=(16, int(config['price_accuracy']))),
438         'code': fields.function(_product_code, method=True, type='char', string='Code'),
439         'partner_ref' : fields.function(_product_partner_ref, method=True, type='char', string='Customer ref'),
440         'default_code' : fields.char('Code', size=64),
441         'active': fields.boolean('Active'),
442         'variants': fields.char('Variants', size=64),
443         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
444         'ean13': fields.char('EAN13', size=13),
445         '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."),
446         'price_extra': fields.float('Variant Price Extra', digits=(16, int(config['price_accuracy']))),
447         'price_margin': fields.float('Variant Price Margin', digits=(16, int(config['price_accuracy']))),
448         'pricelist_id': fields.dummy(string='Pricelist',relation='product.pricelist', type='many2one'),
449     }
450
451     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
452         if uom_id and uom_po_id:
453             uom_obj=self.pool.get('product.uom')
454             uom=uom_obj.browse(cursor,user,[uom_id])[0]
455             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
456             if uom.category_id.id != uom_po.category_id.id:
457                 return {'value': {'uom_po_id': uom_id}}
458         return False
459
460     def _check_ean_key(self, cr, uid, ids):
461         for partner in self.browse(cr, uid, ids):
462             if not partner.ean13:
463                 continue
464             if len(partner.ean13) <> 13:
465                 return False
466             try:
467                 int(partner.ean13)
468             except:
469                 return False
470             sum=0
471             for i in range(12):
472                 if is_pair(i):
473                     sum += int(partner.ean13[i])
474                 else:
475                     sum += 3 * int(partner.ean13[i])
476             check = int(math.ceil(sum / 10.0) * 10 - sum)
477             if check != int(partner.ean13[12]):
478                 return False
479         return True
480
481     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
482
483     def on_order(self, cr, uid, ids, orderline, quantity):
484         pass
485
486     def name_get(self, cr, user, ids, context={}):
487         if not len(ids):
488             return []
489         def _name_get(d):
490             #name = self._product_partner_ref(cr, user, [d['id']], '', '', context)[d['id']]
491             #code = self._product_code(cr, user, [d['id']], '', '', context)[d['id']]
492             name = d.get('name','')
493             code = d.get('default_code',False)
494             if code:
495                 name = '[%s] %s' % (code,name)
496             if d['variants']:
497                 name = name + ' - %s' % (d['variants'],)
498             return (d['id'], name)
499         result = map(_name_get, self.read(cr, user, ids, ['variants','name','default_code'], context))
500         return result
501
502     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
503         if not args:
504             args=[]
505         if not context:
506             context={}
507         if name:
508             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
509             if not len(ids):
510                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
511             if not len(ids):
512                 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
513                 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
514         else:
515             ids = self.search(cr, user, args, limit=limit, context=context)
516         result = self.name_get(cr, user, ids, context)
517         return result
518
519     #
520     # Could be overrided for variants matrices prices
521     #
522     def price_get(self, cr, uid, ids, ptype='list_price', context={}):
523         res = {}
524         product_uom_obj = self.pool.get('product.uom')
525
526         for product in self.browse(cr, uid, ids, context=context):
527             res[product.id] = product[ptype] or 0.0
528             if ptype == 'list_price':
529                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
530                         product.price_extra
531             if 'uom' in context:
532                 uom = product.uos_id or product.uom_id
533                 res[product.id] = product_uom_obj._compute_price(cr, uid,
534                         uom.id, res[product.id], context['uom'])
535         return res
536
537     def copy(self, cr, uid, id, default=None, context=None):
538         if not context:
539             context={}
540
541         product = self.read(cr, uid, id, ['name'], context=context)
542         if not default:
543             default = {}
544         default = default.copy()
545         default['name'] = product['name'] + _(' (copy)')
546
547         if context.get('variant',False):
548             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
549                     'price_margin', 'price_extra']
550             data = self.read(cr, uid, id, fields=fields, context=context)
551             for f in fields:
552                 if f in default:
553                     data[f] = default[f]
554             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
555                     and data['product_tmpl_id'][0]
556             del data['id']
557             return self.create(cr, uid, data)
558         else:
559             return super(product_product, self).copy(cr, uid, id, default=default,
560                     context=context)
561 product_product()
562
563 class product_packaging(osv.osv):
564     _name = "product.packaging"
565     _description = "Packaging"
566     _rec_name = 'ean'
567     _columns = {
568         'sequence': fields.integer('Sequence'),
569         'name' : fields.char('Description', size=64),
570         'qty' : fields.float('Quantity by Package',
571             help="The total number of products you can put by palet or box."),
572         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
573         'ul_qty' : fields.integer('Package by layer'),
574         'rows' : fields.integer('Number of Layer', required=True,
575             help='The number of layer on a palet or box'),
576         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
577         'ean' : fields.char('EAN', size=14,
578             help="The EAN code of the package unit."),
579         'code' : fields.char('Code', size=14,
580             help="The code of the transport unit."),
581         'weight': fields.float('Total Package Weight',
582             help='The weight of a full of products palet or box.'),
583         'weight_ul': fields.float('Empty Package Weight',
584             help='The weight of the empty UL'),
585         'height': fields.float('Height', help='The height of the package'),
586         'width': fields.float('Width', help='The width of the package'),
587         'length': fields.float('Length', help='The length of the package'),
588     }
589
590     _order = 'sequence'
591
592     def name_get(self, cr, uid, ids, context={}):
593         if not len(ids):
594             return []
595         res = []
596         for pckg in self.browse(cr, uid, ids,context=context):
597             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
598             p_name += pckg.ul.name
599             res.append((pckg.id,p_name))
600         return res
601
602     def _get_1st_ul(self, cr, uid, context={}):
603         cr.execute('select id from product_ul order by id asc limit 1')
604         res = cr.fetchone()
605         return (res and res[0]) or False
606
607     _defaults = {
608         'rows' : lambda *a : 3,
609         'sequence' : lambda *a : 1,
610         'ul' : _get_1st_ul,
611     }
612
613     def checksum(ean):
614         salt = '31' * 6 + '3'
615         sum = 0
616         for ean_part, salt_part in zip(ean, salt):
617             sum += int(ean_part) * int(salt_part)
618         return (10 - (sum % 10)) % 10
619     checksum = staticmethod(checksum)
620
621 product_packaging()
622
623
624 class product_supplierinfo(osv.osv):
625     _name = "product.supplierinfo"
626     _description = "Information about a product supplier"
627     _columns = {
628         'name' : fields.many2one('res.partner', 'Partner', required=True, ondelete='cascade', help="Supplier of this product"),
629         '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."),
630         '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."),
631         'sequence' : fields.integer('Priority'),
632         'qty' : fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase for this supplier, expressed in the purchase unit of measure."),
633         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
634         '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."),
635         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
636         'company_id':fields.many2one('res.company','Company'),
637     }
638     _defaults = {
639         'qty': lambda *a: 0.0,
640         'sequence': lambda *a: 1,
641         'delay': lambda *a: 1,
642         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', c)
643     }
644     _order = 'sequence'
645 product_supplierinfo()
646
647
648 class pricelist_partnerinfo(osv.osv):
649     _name = 'pricelist.partnerinfo'
650     _columns = {
651         'name': fields.char('Description', size=64),
652         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
653         'min_quantity': fields.float('Quantity', required=True),
654         'price': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
655     }
656     _order = 'min_quantity asc'
657 pricelist_partnerinfo()
658
659
660
661 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
662