Merge lp:~frederic-declercq/openobject-addons/addons-fu into lp:openobject-addons/extra-trunk

Proposed by Frédéric (Ferme du Sart)
Status: Needs review
Proposed branch: lp:~frederic-declercq/openobject-addons/addons-fu
Merge into: lp:openobject-addons/extra-trunk
Diff against target: 1600 lines (+1490/-0)
21 files modified
network_interactivity/__init__.py (+5/-0)
network_interactivity/__terp__.py (+35/-0)
network_interactivity/netservice.py (+36/-0)
network_interactivity/network.py (+118/-0)
network_interactivity/network_view.xml (+113/-0)
network_interactivity/res.py (+18/-0)
product_multibarcode/__init__.py (+4/-0)
product_multibarcode/__terp__.py (+37/-0)
product_multibarcode/product.py (+298/-0)
product_multibarcode/product_view.xml (+42/-0)
product_multibarcode/res.py (+15/-0)
product_multibarcode/res_view.xml (+21/-0)
product_structure/__init__.py (+5/-0)
product_structure/__terp__.py (+42/-0)
product_structure/alt_osv.py (+51/-0)
product_structure/product.py (+366/-0)
product_structure/product_view.xml (+145/-0)
product_structure/purchase.py (+27/-0)
product_structure/purchase_view.xml (+30/-0)
product_structure/res.py (+47/-0)
product_structure/res_view.xml (+35/-0)
To merge this branch: bzr merge lp:~frederic-declercq/openobject-addons/addons-fu
Reviewer Review Type Date Requested Status
OpenERP Committers Pending
Christophe CHAUVET Pending
Frédéric (Ferme du Sart) Pending
Sharoon Thomas http://openlabs.co.in Pending
OpenERP Core Team Pending
Review via email: mp+16237@code.launchpad.net

This proposal supersedes a proposal from 2009-12-15.

To post a comment you must log in.
Revision history for this message
Raphaël Valyi - http://www.akretion.com (rvalyi) wrote : Posted in a previous version of this proposal

Hello Frederic: one simple question: what makes you believe those module should be distributed in addons rather than extra-addons? or community-addons? Addons are only for modules that are very centric and highly tested on several installations. Before that, modules should bad distributed in the other channels. Thank you for clarifying. NB: I didn't look at your modules themselves.

Revision history for this message
Frédéric (Ferme du Sart) (frederic-declercq) wrote : Posted in a previous version of this proposal

Branch says "addons", but the merge proposal says "addons/extra-trunk". I merge this addons in extra-trunk, then I drop this branch (or use it just for sale/stock/purchase/account proposals).

We will have at least 30 modules to give to community. They've been tested in production in La Ferme du Sart since 3 years on Tiny-4.0, but are recoded to be usefull to all community on latest stable version.

We have done a big work. Now we want to send it to community instead of keeping it for us. Some work won't be supported any more (ie: speeking with Mettler-Toledo scales, ZPL printers... because we moved suppliers). If we think that they could be useful, we will put them into community-addons flagged as "beta". OpenERP should work with us to help having quality.

Our v5 main modules should be in production (so well tested) on the first of april. We plan finishing this big work for summer. I propose you to make further tests if you want to try them before us.

A good documentation will also be on line (graphs, screen captures...) because we will need it for our users. I should put our modules roadmap as blueprints. Some will be assumed by ourselves, but we hope community will want to join us.

Revision history for this message
Christophe CHAUVET (christophe-chauvet) wrote : Posted in a previous version of this proposal

Hi

I've check your proposal, and i see a lot of simple SQL query that can be replace by a browse (or read), You may consider using the osv method instead of SQL Query (if equivalent)

Regards,

review: Needs Fixing
Revision history for this message
Sharoon Thomas http://openlabs.co.in (sharoonthomas) wrote : Posted in a previous version of this proposal

Hi,

As Christophe pointed out there are lot of SQL queries using cr.execute.

Most of them in osv.osv objects are subject to sql injection. Example:line 148 in the diff

I think you need to change them if this has to be usable.

Refer lp:422563 for further details of how your methods may be exploited

Also refer: http://doc.openerp.com/contribute/developing_modules.html?highlight=sql%20injection#security
(Not sure this is efficient enough though)

review: Needs Fixing
Revision history for this message
Frédéric (Ferme du Sart) (frederic-declercq) wrote : Posted in a previous version of this proposal

- all cr.commit() removed
- cr.execute(): removed excessive SQL queries

SQL queries remainding in the code looks like not beeing replace (ie: no 'startswith' operator for search method)

NB security:
All user_id in network.material.type should have few acces to database. But this module doesn't have to check if admin can configure its database.

NB - SQL:
To avoid SQL, I used:
     vals['encoding'] = '%s,%s' % (encoding._table_name, encoding.id)
if lp:~frederic-declercq/openobject-server/server-fu-reference_browse is refused, this should be:
     vals['encoding'] = encoding

review: Needs Resubmitting
Revision history for this message
Numérigraphe (numerigraphe) wrote :

Requesting review from the community again, because this has been pending for a long time.

Unmerged revisions

4196. By Frédéric (Ferme du Sart)

encoding._table to encoding._table_name

4195. By Frédéric (Ferme du Sart)

modified:
  product_multibarcode/product.py

- removed all cr.commit()
- replaced cr.execute() with standard methods when possible

4194. By Frédéric (Ferme du Sart)

New modules:
- network_interactivity (IP management)
- product_multibarcode (weighted barcode)
- product_structure (categories hierarchical structure)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'network_interactivity'
2=== added file 'network_interactivity/__init__.py'
3--- network_interactivity/__init__.py 1970-01-01 00:00:00 +0000
4+++ network_interactivity/__init__.py 2009-12-16 13:50:35 +0000
5@@ -0,0 +1,5 @@
6+# -*- coding: utf-8 -*-
7+
8+import netservice
9+import network
10+import res
11\ No newline at end of file
12
13=== added file 'network_interactivity/__terp__.py'
14--- network_interactivity/__terp__.py 1970-01-01 00:00:00 +0000
15+++ network_interactivity/__terp__.py 2009-12-16 13:50:35 +0000
16@@ -0,0 +1,35 @@
17+# -*- coding: utf-8 -*-
18+
19+{
20+ "name" : "Network interactivity",
21+ "description" :
22+ """
23+Adds actions, online/offline, active, availability to network materials
24+View only hardware stations. Components are listed inside (both are network.material objects)
25+Searching on IP address returns material, not component
26+
27+New netsvc 'network' connection to log in from IP address returning user linked the material type linked to this IP address
28+IP address, and station used in context (res.users context_get() overwrite)
29+
30+NB: Waiting for merge on lp:~frederic-declercq/openobject-server/server-fu-ip_address to get IP address
31+__________________________________________
32+=> descriptions & screenshots:
33+ http://www.lafermedusart.com/modules/network_interactivity.html
34+
35+All modules from Fermes Urbaines are put on launchpad when used on production.
36+We just offer support to Fermes Urbaines partners.
37+Eventhought, all addons are updated as soon as tested.
38+""",
39+ "version" : "1.0.0",
40+ "depends" : [ "base", "network" ],
41+ "author" : "Fermes Urbaines",
42+ "website" : "http://www.lafermedusart.com/modules/network_interactivity.html",
43+ "category" : "Generic Modules/Interfaces",
44+ "init_xml" : [ ],
45+ "demo_xml" : [ ],
46+ "update_xml" : [
47+ 'network_view.xml',
48+ ],
49+ "active": True,
50+ "installable" : True
51+}
52
53=== added file 'network_interactivity/netservice.py'
54--- network_interactivity/netservice.py 1970-01-01 00:00:00 +0000
55+++ network_interactivity/netservice.py 2009-12-16 13:50:35 +0000
56@@ -0,0 +1,36 @@
57+# -*- coding: utf-8 -*-
58+import netsvc, sql_db
59+
60+class network_interface(netsvc.Service):
61+
62+ def __init__(self,name="network"):
63+ netsvc.Service.__init__(self,name)
64+ self.joinGroup("web-services")
65+ self.exportMethod(self.ip_login)
66+ self.exportMethod(self.code39_login)
67+
68+ def ip_login(self, ip_address, dbname):
69+ # IP address is provided by querying. Only DB name required for hardware identified on network.material with user_id set on material type
70+ logger = netsvc.Logger()
71+ if not (dbname and ip_address): return False
72+ ip_ok = True
73+ try:
74+ for n in map(int,ip_address.split('.')):
75+ if n<0 or n>254: ip_ok = False
76+ except: ip_ok = False
77+ if not ip_ok:
78+ logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: %s is not a valid IP address' % ip_address)
79+ return False
80+ cr= sql_db.db_connect(dbname).cursor()
81+ cr.execute("""SELECT u.id, u.password
82+ FROM network_material m, network_hardware_type t, res_users u
83+ WHERE t.id=m.type AND u.id=t.user_id AND m.ip_addr='%s'""" % ip_address)
84+ if not cr.rowcount:
85+ logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: Access denied for %s' % ip_address)
86+ return False
87+ logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: Successful login for %s' % ip_address)
88+ uid, password= cr.fetchone()
89+ cr.close()
90+ return uid, password
91+
92+network_interface()
93
94=== added file 'network_interactivity/network.py'
95--- network_interactivity/network.py 1970-01-01 00:00:00 +0000
96+++ network_interactivity/network.py 2009-12-16 13:50:35 +0000
97@@ -0,0 +1,118 @@
98+# -*- coding: utf-8 -*-
99+
100+from osv import fields, osv
101+import time
102+
103+class network_hardware_type(osv.osv):
104+ _name = "network.hardware.type"
105+ _inherit = "network.hardware.type"
106+
107+ _columns = {
108+ 'user_id': fields.many2one('res.users', 'User', help="This user is the default one used when logging from IP for this kind of hardware. Let empty if not a computer quering OpenERP with a special client interface"),
109+ }
110+
111+network_hardware_type()
112+
113+class network_network(osv.osv):
114+ _name = 'network.network'
115+ _inherit = 'network.network'
116+
117+ _columns = {
118+ 'active': fields.boolean('Active'),
119+ }
120+
121+ _defaults = {
122+ 'active': lambda *a: True,
123+ }
124+
125+network_network()
126+
127+class network_material(osv.osv):
128+ _name = "network.material"
129+ _inherit = "network.material"
130+
131+ _columns = {
132+ 'active': fields.boolean('Active'),
133+ 'online': fields.boolean('On line'),
134+ 'available': fields.boolean('Available'),
135+ 'action_ids': fields.one2many('network.material.action', 'material_id', 'Actions', domain=[('date','>=',time.strftime('%Y-%m-%d 00:00:00'))]),
136+ }
137+
138+ _defaults = {
139+ 'online': lambda *a: True,
140+ 'active': lambda *a: True,
141+ 'available': lambda *a: True,
142+ }
143+
144+ def check_busy(self, cr, uid, ids, context=None):
145+ ret = {}
146+ if not ids: return ret
147+ for i in ids: ret[i] = False
148+ cr.execute("SELECT material_id, MIN(id) FROM network_material_action WHERE date_done IS NULL AND material_id IN (%s) GROUP BY 1" % ','.join(map(str,ids)))
149+ if cr.rowcount:
150+ for r in cr.fetchall(): ret[r[0]] = r[1]
151+ avail_ids = [ i for i in ret if not ret[i] ]
152+ if avail_ids:
153+ self.pool.get('network.material').write(cr, uid, avail_ids, {'available': True})
154+ return ret
155+
156+ def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
157+ # on IP search, return computers, not components
158+ rargs = []
159+ for arg in args:
160+ if arg[0] == 'ip_addr':
161+ component_ids = super(network_material, self).search(cr, uid, [arg], offset=offset, limit=limit, order=order, context=context, count=count)
162+ computer_ids = []
163+ for material in self.browse(cr, uid, component_ids, context=context):
164+ while material.parent_id: material = material.parent_id
165+ computer_ids.append( material.id )
166+ rargs.append( ('id', 'in', list(set( computer_ids ))) )
167+ else: rargs.append( arg )
168+ args = rargs
169+
170+ # view just computers, not components
171+ if not 'parent_id' in [arg[0] for arg in args]:
172+ args.append( ('parent_id', '=', False) )
173+ return super(network_material, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
174+
175+network_material()
176+
177+def _action_models_get(obj, cr, uid, context={}):
178+ mids = obj.pool.get('ir.model').search(cr, uid, [('model','in', obj._ref_tables)])
179+ return [ (r['model'],r['name']) for r in obj.pool.get('ir.model').read(cr, uid, mids, ['model','name'], context=context) ]
180+
181+class network_material_action(osv.osv):
182+ _name = 'network.material.action'
183+ _description = 'Actions done on hardware'
184+
185+ _ref_tables = [ 'pos.order', 'sale.order', 'stock.inventory', 'stock.picking', 'purchase.order', 'account.invoice' ]
186+
187+ _columns = {
188+ 'name': fields.char('Action', size=64, required=True),
189+ 'ref': fields.reference('Reference', selection=_action_models_get, size=128),
190+ 'material_id': fields.many2one('network.material', 'Material', required=True),
191+ 'date': fields.datetime('Started at', required=True),
192+ 'date_done': fields.datetime('Done at'),
193+ }
194+
195+ _order = 'date desc'
196+
197+ _defaults = {
198+ 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
199+ }
200+
201+ def unlink(self, cr, uid, ids, context=None):
202+ if not ids: return True
203+ self.write(cr, uid, ids, {'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
204+ mids = [ r['material_id'][0] for r in self.read(cr, uid, ids, ['material_id']) ]
205+ self.pool.get('network.material').check_busy(cr, uid, mids, context=context)
206+ return True
207+
208+ def create(self, cr, uid, vals, context=None):
209+ if not 'date' in vals: vals['date'] = time.strftime('%Y-%m-%d %H:%M:%S')
210+ if not 'name' in vals: vals['name'] = vals['ref']
211+ ret = super(network_material_actions, self).create(cr, uid, vals, context=context)
212+ self.pool.get('network.material').write(cr, uid, [ vals['material_id'], ], {'available': False})
213+ return ret
214+
215+network_material_action()
216
217=== added file 'network_interactivity/network_view.xml'
218--- network_interactivity/network_view.xml 1970-01-01 00:00:00 +0000
219+++ network_interactivity/network_view.xml 2009-12-16 13:50:35 +0000
220@@ -0,0 +1,113 @@
221+<?xml version="1.0"?>
222+<openerp>
223+ <data>
224+
225+ <record model="ir.ui.view" id="edit_network2">
226+ <field name="name">network.material.form</field>
227+ <field name="model">network.material</field>
228+ <field name="type">form</field>
229+ <field name="inherit_id" ref="network.edit_network"/>
230+ <field name="arch" type="xml">
231+ <notebook position="inside">
232+ <page string="Elements">
233+ <field name="child_id" nolabel="1" colspan="4"/>
234+ </page>
235+ <page string="Actions">
236+ <field name="online" select="1"/>
237+ <field name="available" select="1"/>
238+ <field name="action_ids" nolabel="1" colspan="4"/>
239+ </page>
240+ </notebook>
241+ </field>
242+ </record>
243+
244+ <record model="ir.ui.view" id="edit_network3">
245+ <field name="name">network.material.form</field>
246+ <field name="model">network.material</field>
247+ <field name="type">form</field>
248+ <field name="inherit_id" ref="network.edit_network"/>
249+ <field name="arch" type="xml">
250+ <field name="network_id" position="after">
251+ <field name="active" select="1"/>
252+ </field>
253+ </field>
254+ </record>
255+
256+
257+ <record model="ir.ui.view" id="material_view">
258+ <field name="name">network.material.tree</field>
259+ <field name="model">network.material</field>
260+ <field name="priority" eval="1"/>
261+ <field name="type">tree</field>
262+ <field name="field_parent">child_id</field>
263+ <field name="arch" type="xml">
264+ <tree string="Network Material">
265+ <field name="name"/>
266+ <field name="ip_addr"/>
267+ <field name="active" />
268+ <field name="online"/>
269+ <field name="available" />
270+ </tree>
271+ </field>
272+ </record>
273+
274+
275+
276+ <record model="ir.ui.view" id="view_hardware_type_form">
277+ <field name="name">network.hardware.type.form</field>
278+ <field name="model">network.hardware.type</field>
279+ <field name="type">form</field>
280+ <field name="inherit_id" ref="network.view_hardware_type_form"/>
281+ <field name="arch" type="xml">
282+ <field name="networkable" position="after">
283+ <field name="user_id" select="1"/>
284+ </field>
285+ </field>
286+ </record>
287+
288+ </data>
289+ <data noupdate="1">
290+
291+ <record forcecreate="True" id="server_type" model="network.hardware.type">
292+ <field name="name">Server</field>
293+ <field name="networkable">False</field>
294+ </record>
295+ <record forcecreate="True" id="client_type" model="network.hardware.type">
296+ <field name="name">Client computer</field>
297+ <field name="networkable">False</field>
298+ </record>
299+ <record forcecreate="True" id="eth_type" model="network.hardware.type">
300+ <field name="name">Ethernet</field>
301+ <field name="networkable">True</field>
302+ </record>
303+ <record forcecreate="True" id="lo_type" model="network.hardware.type">
304+ <field name="name">Loopback</field>
305+ <field name="networkable">True</field>
306+ <field name="user_id">1</field>
307+ </record>
308+ <record forcecreate="True" id="wifi_type" model="network.hardware.type">
309+ <field name="name">WiFi</field>
310+ <field name="networkable">True</field>
311+ </record>
312+
313+ <record forcecreate="True" id="localhost" model="network.network">
314+ <field name="name">localhost</field>
315+ <field name="range">127.0.0</field>
316+ <field name="contact_id" ref="base.main_address"/>
317+ </record>
318+
319+ <record forcecreate="True" id="self_server" model="network.material">
320+ <field name="name">Main server</field>
321+ <field name="user_id">Main server</field>
322+ <field name="type" ref="server_type"/>
323+ </record>
324+ <record forcecreate="True" id="self_server_ip" model="network.material">
325+ <field name="name">Main server loopback</field>
326+ <field name="ip_addr">127.0.0.1</field>
327+ <field name="type" ref="lo_type"/>
328+ <field name="network_id" ref="localhost"/>
329+ <field name="parent_id" ref="self_server"/>
330+ </record>
331+
332+ </data>
333+</openerp>
334
335=== added file 'network_interactivity/res.py'
336--- network_interactivity/res.py 1970-01-01 00:00:00 +0000
337+++ network_interactivity/res.py 2009-12-16 13:50:35 +0000
338@@ -0,0 +1,18 @@
339+# -*- coding: utf-8 -*-
340+
341+from osv import fields, osv
342+
343+class res_users( osv.osv ):
344+ _name = 'res.users'
345+ _inherit = 'res.users'
346+
347+ def context_get(self, cr, uid, context=None):
348+ ret = super(res_users, self).context_get(cr, uid, context=context)
349+ if not (context or {}).get('ip_address', False): return ret
350+ ret['ip_address'] = context['ip_address']
351+ material_ids = self.pool.get('network.material').search(cr, uid, [('ip_addr','=',context['ip_address'])])
352+ if not material_ids: return ret
353+ ret['material_id'] = material_ids[0]
354+ return ret
355+
356+res_users()
357
358=== added directory 'product_multibarcode'
359=== added file 'product_multibarcode/__init__.py'
360--- product_multibarcode/__init__.py 1970-01-01 00:00:00 +0000
361+++ product_multibarcode/__init__.py 2009-12-16 13:50:35 +0000
362@@ -0,0 +1,4 @@
363+# -*- coding: utf-8 -*-
364+
365+import res
366+import product
367\ No newline at end of file
368
369=== added file 'product_multibarcode/__terp__.py'
370--- product_multibarcode/__terp__.py 1970-01-01 00:00:00 +0000
371+++ product_multibarcode/__terp__.py 2009-12-16 13:50:35 +0000
372@@ -0,0 +1,37 @@
373+# -*- coding: utf-8 -*-
374+
375+{
376+ "name" : "Product ean8 & 13 barcodes with weight",
377+ "description" :
378+ """
379+- Weight barcode depending on currency (French FRF, Euro...) or weight include in barcode
380+- Price calculation
381+- Ean automated creation
382+- Key calculation
383+- Many barcodes depending on packaging (ie: 1 lot of 6 milk packs)
384+- ean13 field as a function for compatibility
385+- ean13 search on product fields
386+- methods to get quantity / price (ie: for use with POS)
387+- ean13.ttf encoding
388+__________________________________________
389+=> descriptions & screenshots:
390+ http://www.fermes-urbaines.com/openerp/modules/product_multibarcode.html
391+
392+All modules from Fermes Urbaines are put on launchpad when used on production.
393+We just offer support to Fermes Urbaines partners.
394+Eventhought, all addons are updated as soon as tested.
395+""",
396+ "version" : "1.0.0",
397+ "depends" : [ "product_tax_incl" ],
398+ "author" : "Fermes Urbaines",
399+ "website" : "http://www.fermes-urbaines.com/openerp/modules/product_multibarcode.html",
400+ "category" : "Generic Modules/Base",
401+ "init_xml" : [ ],
402+ "demo_xml" : [ ],
403+ "update_xml" : [
404+ "res_view.xml",
405+ "product_view.xml"
406+ ],
407+ "active": True,
408+ "installable" : True
409+}
410
411=== added file 'product_multibarcode/product.py'
412--- product_multibarcode/product.py 1970-01-01 00:00:00 +0000
413+++ product_multibarcode/product.py 2009-12-16 13:50:35 +0000
414@@ -0,0 +1,298 @@
415+# -*- coding: utf-8 -*-
416+
417+from osv import fields, osv
418+import re
419+
420+# ean.ttf translation tables:
421+TTF_EANTABLES=[
422+ ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'),
423+ ('K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'),
424+ ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j')]
425+TTF_EAN6=['000000', '001011', '001101', '001110', '010011', '011001', '011100', '010101', '010110', '011010']
426+
427+class product_packaging( osv.osv ):
428+ _name = "product.packaging"
429+ _inherit = "product.packaging"
430+
431+ # check if unique (weight/price + unit)
432+ def _unique_ean(self, cr, uid, ids):
433+ for b in self.browse(cr,uid,ids):
434+ if b.ean and len(self.search(cr, uid, [('ean','=',b.ean)]))>1: return False
435+ return True
436+
437+ # check EAN syntax
438+ def _check_ean(self, cr, uid, ids):
439+ for b in self.browse(cr, uid, ids):
440+ if b.ean:
441+ if re.sub('[0-9]','',b.ean) or (len(b.ean)<7) or (len(b.ean)<12 and not b.encoding):
442+ return False
443+ return True
444+
445+ # ean.ttf translation: use this to write barcode with this font
446+ def _ttf(self, cr, uid, ids, name, arg, context=None):
447+ ret= {}
448+ for b in self.browse(cr, uid, ids):
449+ if b.ean:
450+ tb=TTF_EAN6[int(b.ean[:1])].replace('A','1').replace('B','2')
451+ txt=b.ean[:1]
452+ i=1
453+ while i<7:
454+ txt+=TTF_EANTABLES[int(tb[i-1:i])][int(b.ean[i:i+1])]
455+ i+=1
456+ txt+='*'
457+ while i<13:
458+ txt+=TTF_EANTABLES[2][int(b.ean[i:i+1])]
459+ i+=1
460+ ret[b.id]=txt+'+'
461+ else:
462+ ret[b.id]=''
463+ return ret
464+
465+ _columns= {
466+ "encoding": fields.reference('Encoding', selection=[('res.currency','Currency'),('product.uom','Unit')], size=128, select=2, help="Weight or price base to encode weight (let empty if product price doesn't depend on weight)"),
467+ "ttf": fields.function(_ttf, method=True, type='char', size=20, string='TTF', readonly=True, store=True, help="Encryption to use with ean3.ttf")
468+ }
469+ _defaults= {
470+ "ean": lambda obj,cr,uid,context={}: obj.new_ean( cr, uid, [], encoding=False),
471+ }
472+ _constraints= [
473+ (_check_ean, 'This barcode is not EAN8 or EAN13', ['ean']),
474+ (_unique_ean, 'This barcode is already used', ['ean','product_id','encoding'])
475+ ]
476+
477+ def onchange_encoding(self, cr, uid, ids, encoding):
478+ return {'values': {'ean': self.new_ean( cr, uid, ids, encoding=encoding)} }
479+
480+ # creates an internal barcode (starting with 2) reaching first available, caring if weighted
481+ # NB: You have to pay for a GS1 certified barcode if you don't have your own shop or if you plan to sale to other shops
482+ def new_ean( self, cr, uid, ids, encoding=False):
483+ if encoding:
484+ cr.execute("""SELECT e1.ean
485+ FROM (SELECT (LPAD(ean,7)::bigint+1)::varchar||'00000' AS ean FROM product_packaging WHERE ean ILIKE '2%%'
486+ UNION SELECT '200000000000' AS ean) e1
487+ LEFT JOIN product_packaging e2 ON LPAD(e2.ean,7)=LPAD(e1.ean,7)
488+ WHERE e2.ean IS NULL
489+ ORDER BY 1
490+ LIMIT 1""")
491+ else:
492+ cr.execute("""SELECT e1.ean
493+ FROM (SELECT (LPAD(ean,12)::bigint+1)::varchar AS ean FROM product_packaging WHERE ean ILIKE '2%%'
494+ UNION SELECT (LPAD(ean,7)::bigint+1)::varchar||'00000' AS ean FROM product_packaging WHERE ean ILIKE '2%%'
495+ UNION SELECT '200000000000' AS ean) e1
496+ LEFT JOIN product_packaging e2 ON LPAD(e2.ean,12)=e1.ean AND e2.encoding IS NULL
497+ LEFT JOIN product_packaging e3 ON LPAD(e3.ean,7)=LPAD(e1.ean,7) AND e3.encoding IS NOT NULL
498+ WHERE e2.ean IS NULL AND e3.ean IS NULL
499+ ORDER BY 1
500+ LIMIT 1""")
501+ return self.ean_key(cr, uid, str(cr.fetchone()[0]))
502+
503+ # builds EAN13 key
504+ def ean_key(self, cr, uid, ean):
505+ ean= ean[:12]
506+ n=0
507+ i=0
508+ while i<len(ean):
509+ n+= int(ean[i:i+1])
510+ i+=2
511+ i=1
512+ while i<len(ean):
513+ n+= int(ean[i:i+1])*3
514+ i+=2
515+ key= 10- (n % 10)
516+ if key==10: key=0
517+ return ean+str(key)
518+
519+ # reaches weight encoding (default on res_company) if not set
520+ # removes encoding if no decimal unit set (depending on rounding of uos_id / uom_id)
521+ # sets key (ignore if allready set)
522+ def create(self, cr, uid, vals, context=None):
523+ e= vals.get('ean',False)
524+ if e:
525+ res=self.pool.get("product.product").read(cr, uid, [vals['product_id']], ['name','ean_type'])[0]
526+ unit_ean= (str(res['ean_type'])=='unit')
527+ if unit_ean: vals['encoding']=False
528+ else:
529+ e= e[:7]
530+ if not vals.get('encoding',False):
531+ # this code has been changed to avoid SQL, but depends on lp:~frederic-declercq/openobject-server/server-fu-reference_browse
532+ encoding = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ean_encoding
533+ vals['encoding']= '%s,%s' % (encoding._table_name, encoding.id)
534+ e+=('0'*(12-len(e)))
535+ vals['ean']= self.ean_key(cr, uid, e)
536+ return super(product_packaging, self).create(cr, uid, vals, context=context)
537+
538+ # reaches barcode through unit and weight EANs
539+ def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
540+ rargs=[]
541+ for arg in args:
542+ if arg[0]=='ean':
543+ # SQL required because 'startswith' isn't a valid operator
544+ cr.execute("SELECT id FROM product_packaging WHERE ean like '%s%%' OR (ean like '%s%%' AND encoding IS NOT NULL)" % (arg[2][:12], arg[2][:7]))
545+ if len(args)==1: return [r[0] for r in cr.fetchall()]
546+ rargs.append( ('id','in',[r[0] for r in cr.fetchall()]) )
547+ else: rargs.append(arg)
548+ return super(product_packaging, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
549+
550+ # function returning product id, name, ean type (price, weight, unit), price, quantity, amount from barcode
551+ def ean_data(self, cr, uid, ean, pricelist_id, tax_included=True, partner_id=None, address_id=None, context={}):
552+ ret= []
553+ ids= self.search(cr, uid, [('ean','=',ean)])
554+ if not ids: return ret
555+ data_part= int(ean[7:12])
556+ for b in self.browse(cr, uid, ids, context=context):
557+ if b.encoding:
558+ #obj,oid= b.encoding.split(',')
559+ #e=self.pool.get(obj).browse(cr,uid,int(str(oid)))
560+ r=[]
561+ if b.encoding.rate:
562+ # no use to apply lot prices on products with weight: scales can't use OpenERP pricelists
563+ r= {
564+ 'id': b.product_id.id,
565+ 'name': b.product_id.name,
566+ 'type': 'price',
567+ 'uom_id': b.product_id.uom_id.id,
568+ 'amount': round(0.01*data_part/b.encoding.rate, 2),
569+ 'quantity': round( (0.01*data_part/b.encoding.rate) /b.product_id.list_price_tax_incl, 3 ) }
570+ r['price']= self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], b.product_id.id, r['quantity'], partner_id, context=context)[pricelist_id]
571+ else:
572+ r= {
573+ 'id': b.product_id.id,
574+ 'name': b.product_id.name,
575+ 'type': 'weight',
576+ 'uom_id': b.product_id.uom_id.id,
577+ 'quantity': round(1.0*data_part/b.encoding.factor, b.encoding.rounding) }
578+ r['price']= self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], b.product_id.id, r['quantity'], partner_id, context=context)[pricelist_id]
579+ r['amount']= r['price']*r['quantity']
580+ else:
581+ pptype = self.pool.get('product.pricelist').read(cr, uid, [pricelist_id], ['type'])[0]['type']
582+ uom= (pptype=='purchase') and b.product_id.uom_po_id.id or ((b.product_id.uos_id) and b.product_id.uos_id.id or b.product_id.uom_id.id)
583+ pkg_qty= b.qty*b.ul_qty*b.rows
584+ r= {
585+ 'id': b.product_id.id,
586+ 'name': b.product_id.name,
587+ 'type': 'unit',
588+ 'uom_id': b.product_id.uom_id.id,
589+ 'quantity': (b.product_id.uom_id.id==uom) \
590+ and pkg_qty or \
591+ self.pool.get('product.uom')._compute_qty(cr, uid, uom, pkg_qty, b.product_id.uom_id.id),
592+ 'price': self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], b.product_id.id, 1.0, partner_id, context=context)[pricelist_id] }
593+ r['amount']= r['price']
594+ if tax_included:
595+ taxes_id = self.pool.get('product.product').read(cr, uid, [r['id']],['taxes_id'])[0]['taxes_id']
596+ taxes= self.pool.get( 'account.tax' ).browse(cr, uid, taxes_id, context=context)
597+ taxes= self.pool.get( 'account.tax' ).compute( cr, uid, taxes, r['price'], r['quantity'], address_id, r['id'], partner_id )
598+ r['price'] += sum([t['amount'] for t in taxes])
599+ if r['type']!='price':
600+ # on amount written into barcode, we have to respect amount
601+ r['amount']= r['price']*r['quantity']
602+ ret.append(r)
603+ return ret
604+
605+product_packaging()
606+
607+class product_product( osv.osv ):
608+ _name= "product.product"
609+ _inherit= "product.product"
610+
611+ # check if unit or weight depending on uos / uom rounding
612+ def _ean_type( self, cr, uid, ids, name, args, context={} ):
613+ ret = {}
614+ if not ids: return ret
615+ for prod in self.browse(cr, uid, ids, context=context):
616+ uom = prod.uos_id or prod.uom_id
617+ ret[prod.id] = (uom.rounding == int(uom.rounding)) and 'unit' or 'price'
618+ return ret
619+
620+ # old ean13 field becomes a function returning the first ean
621+ def _ean13( self, cr, uid, ids, name, args, context={} ):
622+ ret = {}
623+ for b in self.browse(cr, uid, ids):
624+ ret[b.id]=(b.packaging) and b.packaging[0].ean or False
625+ return ret
626+
627+ _columns = {
628+ "ean13": fields.function(_ean13, method=True, type='char', size=20, string='EAN13', readonly=True, store=False),
629+ "ean_type": fields.function(_ean_type, method=True, type='selection', selection=[('unit','Unit'),('price','Price')], string='EAN type', readonly=True),
630+ }
631+
632+ # reaches any barcode from name field
633+ def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
634+ rargs=[]
635+ for arg in args:
636+ if (arg[0]=='name') and not re.sub('[0-9]','',arg[2]):
637+ # SQL required because 'startswith' isn't recognized as operator
638+ cr.execute("SELECT product_id FROM product_packaging WHERE ean like '%s%%' OR (ean like '%s%%' AND encoding IS NOT NULL) GROUP BY 1" % (arg[2][:12], arg[2][:7]))
639+ if len(args)==1: return [r[0] for r in cr.fetchall()]
640+ rargs.append( ('id','in',[r[0] for r in cr.fetchall()]) )
641+ else: rargs.append(arg)
642+ return super(product_product, self).search(cr, uid, rargs, offset=offset, limit=limit, order=order, context=context, count=count)
643+
644+ def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
645+ if not args:
646+ args=[]
647+ if not context:
648+ context={}
649+ if name:
650+ ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
651+ if not len(ids):
652+ ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
653+ ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
654+ else:
655+ ids = self.search(cr, user, args, limit=limit, context=context)
656+ result = self.name_get(cr, user, ids, context)
657+ return result
658+
659+ # if no barcode set on create, it creates one (if sale_ok)
660+ def create( self , cr, uid, vals, context={} ):
661+ if vals.get('sale_ok',False) and not vals.get('packaging',[]):
662+ uom_rounding = self.pool.get('product.uom').read(cr, uid, [vals.get('uos_id',False) or vals.get('uom_id',False)], ['rounding'])[0]['rounding']
663+ ean_unit = uom_rounding == int(uom_rounding)
664+ ean= self.pool.get('product.packaging').new_ean(cr,uid,[], ean_unit)
665+ ul = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ul.id
666+ vals['packaging']=[(0,0,{'rows':1, 'sequence': 1, 'ean': ean, 'ul': ul, 'qty': 1, 'ul_qty': 1})]
667+ ret= super( product_product, self ).create(cr,uid,vals, context=context )
668+ return ret
669+
670+ # on obsolete, drops all eans for this product
671+ # on uos / uom change if ean type changed, checks eans and corrects if required
672+ def write( self , cr, uid, ids, vals, context={} ):
673+ if vals.get('state','draft')=='obsolete':
674+ ret= super( product_product, self ).write( cr, uid, ids, vals, context=context )
675+ pp_ids = self.pool.get('product.packaging').search(cr, uid, [('product_id','in',ids)], context=context)
676+ self.pool.get('product.packaging').unlink(cr, uid, ppids, context=context)
677+ return ret
678+ types= {}
679+ for b in self.browse(cr, uid, ids):
680+ types[b.id]= b.ean_type
681+ ean_alert= False
682+ for n in vals.get('packaging',[]):
683+ if n[0] and n[2].get('ean',False): ean_alert=True
684+ if ean_alert:
685+ if 'packaging' in vals:
686+ super( product_product, self ).write( cr,uid,ids,{'packaging':vals['packaging']}, context=context )
687+ del vals['packaging']
688+ for r in self.browse(cr, uid, ids,context=context):
689+ if not r.packaging:
690+ uom = r.uos_id or r.uom_id
691+ uom_rounding = uom.rounding==int(uom.rounding)
692+ ean= self.pool.get('product.packaging').new_ean(cr,uid,[], uom_rounding)
693+ ul = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ul.id
694+ cr.execute("""SELECT seq
695+ FROM (SELECT 1 AS seq, %d AS product_id UNION SELECT sequence+1 AS seq, product_id FROM product_packaging WHERE product_id=%d) s
696+ LEFT JOIN product_packaging p ON p.sequence=s.seq AND p.product_id=s.product_id
697+ WHERE p.sequence IS NULL
698+ ORDER BY 1 DESC LIMIT 1""" % (r.id,r.id))
699+ seq= cr.fetchone()[0]
700+ v = {'packaging': [(0,0,{'rows':1, 'sequence': seq, 'ean': ean, 'ul': ul, 'qty': 1, 'ul_qty': 1})] }
701+ super( product_product, self ).write( cr,uid,r.id, v, context=context )
702+ ret= super( product_product, self ).write( cr,uid,ids,vals, context=context )
703+ return ret
704+
705+ # on delete, drops all eans for this product
706+ # for security, sets ean13 to null too
707+ def unlink( self, cr, uid, ids, context={} ):
708+ pp_ids = self.pool.get('product.packaging').search(cr, uid, [('product_id', 'in', ids)])
709+ self.pool.get('product.packaging').unlink(cr, uid, pp_ids, context=context)
710+ return super( product_product, self ).unlink(cr, uid, ids, context=context)
711+
712+product_product()
713
714=== added file 'product_multibarcode/product_view.xml'
715--- product_multibarcode/product_view.xml 1970-01-01 00:00:00 +0000
716+++ product_multibarcode/product_view.xml 2009-12-16 13:50:35 +0000
717@@ -0,0 +1,42 @@
718+<?xml version="1.0" ?>
719+<openerp>
720+ <data>
721+
722+ <record id="product_normal_form_view" model="ir.ui.view">
723+ <field name="name">product.normal.form</field>
724+ <field name="model">product.product</field>
725+ <field name="inherit_id" ref="product.product_normal_form_view"/>
726+ <field name="type">form</field>
727+ <field name="arch" type="xml">
728+ <field name="packaging" position="replace">
729+ <label string="" colspan="2"/>
730+ <field name="ean_type" colspan="2" readonly="1"/>
731+ <field colspan="4" name="packaging" nolabel="1">
732+ <form string="Packaging">
733+ <field name="ean" select="1"/>
734+ <field name="ttf"/>
735+ <newline/>
736+ <field name="sequence"/>
737+ <field name="encoding" on_change="onchange_encoding(encoding)"/>
738+ <newline/>
739+ <field name="qty" select="1"/>
740+ <field name="ul"/>
741+ <field name="weight_ul"/>
742+ <separator colspan="4" string="Palletization"/>
743+ <field name="ul_qty"/>
744+ <field name="rows"/>
745+ <field name="weight"/>
746+ <separator colspan="4" string="Pallet Dimension"/>
747+ <field name="height"/>
748+ <field name="width"/>
749+ <field name="length"/>
750+ <separator colspan="4" string="Other Info"/>
751+ <field colspan="4" name="name" select="1"/>
752+ </form>
753+ </field>
754+ </field>
755+ </field>
756+ </record>
757+
758+ </data>
759+</openerp>
760
761=== added file 'product_multibarcode/res.py'
762--- product_multibarcode/res.py 1970-01-01 00:00:00 +0000
763+++ product_multibarcode/res.py 2009-12-16 13:50:35 +0000
764@@ -0,0 +1,15 @@
765+# -*- coding: utf-8 -*-
766+
767+from osv import fields, osv
768+
769+class res_company( osv.osv ):
770+ _name= "res.company"
771+ _inherit= "res.company"
772+ _columns= {
773+ # default object to base method for encoding weighted barcodes
774+ # if currency: 0.01 * rate * [ EAN data ]
775+ # if unit: factor * [ EAN data ]
776+ "ean_encoding": fields.reference('EAN weight', selection=[('res.currency','Currency'),('product.uom','Unit')], size=128, required=True, help="Default base for EAN13 weight encoding"),
777+ 'ul' : fields.many2one('product.ul', 'Type of Package', required=True, help="Shipping unit returning the product unit of measure (default, purchase or sale) set on product"),
778+ }
779+res_company()
780
781=== added file 'product_multibarcode/res_view.xml'
782--- product_multibarcode/res_view.xml 1970-01-01 00:00:00 +0000
783+++ product_multibarcode/res_view.xml 2009-12-16 13:50:35 +0000
784@@ -0,0 +1,21 @@
785+<?xml version="1.0" ?>
786+<openerp>
787+ <data>
788+
789+ <record id="view_company_form" model="ir.ui.view">
790+ <field name="name">res.company.form</field>
791+ <field name="model">res.company</field>
792+ <field name="inherit_id" ref="base.view_company_form"/>
793+ <field name="type">form</field>
794+ <field name="arch" type="xml">
795+ <page string="Configuration" position="inside">
796+ <separator colspan="4" string="Barcodes"/>
797+ <field name="ean_encoding"/>
798+ <field name="ul"/>
799+ </page>
800+ </field>
801+ </record>
802+
803+ </data>
804+</openerp>
805+
806
807=== added directory 'product_structure'
808=== added file 'product_structure/__init__.py'
809--- product_structure/__init__.py 1970-01-01 00:00:00 +0000
810+++ product_structure/__init__.py 2009-12-16 13:50:35 +0000
811@@ -0,0 +1,5 @@
812+# -*- coding: utf-8 -*-
813+
814+import product
815+import purchase
816+import res
817\ No newline at end of file
818
819=== added file 'product_structure/__terp__.py'
820--- product_structure/__terp__.py 1970-01-01 00:00:00 +0000
821+++ product_structure/__terp__.py 2009-12-16 13:50:35 +0000
822@@ -0,0 +1,42 @@
823+# -*- coding: utf-8 -*-
824+
825+{
826+ "name" : "Product structure",
827+ "description" :
828+ """
829+Adds typed multi levels human readable structure on product categories
830+
831+Defaults values for products depending on categories & children:
832+- tax
833+- payment term
834+- email
835+- product manager
836+
837+Adds also:
838+- product categories on partners
839+- product category on purchase orders with search on category & supplier for products
840+- products list on partner from supplierinfo
841+- partners on supplierinfo found as suppliers even if not set
842+__________________________________________
843+=> descriptions & screenshots:
844+ http://www.lafermedusart.com/modules/product_structure.html
845+
846+All modules from Fermes Urbaines are put on launchpad when used on production.
847+We just offer support to Fermes Urbaines partners.
848+Eventhought, all addons are updated as soon as tested.
849+""",
850+ "version" : "1.0.0",
851+ "depends" : [ "purchase" ],
852+ "author" : "Fermes Urbaines",
853+ "website" : "http://www.lafermedusart.com/modules/product_structure.html",
854+ "category" : "Generic Modules/Base",
855+ "init_xml" : [ ],
856+ "demo_xml" : [ ],
857+ "update_xml" : [
858+ "product_view.xml",
859+ "purchase_view.xml",
860+ "res_view.xml"
861+ ],
862+ "active": True,
863+ "installable" : True
864+}
865
866=== added file 'product_structure/alt_osv.py'
867--- product_structure/alt_osv.py 1970-01-01 00:00:00 +0000
868+++ product_structure/alt_osv.py 2009-12-16 13:50:35 +0000
869@@ -0,0 +1,51 @@
870+# -*- coding: utf-8 -*-
871+
872+from osv import osv
873+
874+class alt_osv(osv.osv):
875+ """Proposal for an extension on osv.osv:
876+
877+All fields in _recursive list have recursion value. To avoid recursion (ie to configure), you can set {'recursion': False} in context.
878+Recursion can be done if special field 'parent_id' is on recursion model."""
879+
880+ _recursive = []
881+
882+ def read(self, cr, uid, ids, field_names=None, context=None, load='_classic_read'):
883+ """Browse() depends on read(). Changing read() changes browse()."""
884+
885+ columns = self._columns.keys() + self._inherit_fields.keys()
886+ if (not 'parent_id' in columns) or (not self._recursive):
887+ return super(alt_osv, self).read(cr, uid, ids, fields=field_names, context=context, load=load)
888+
889+ if not context: context = {}
890+ delparent = False
891+ if field_names and not 'parent_id' in field_names:
892+ field_names.append( 'parent_id' )
893+ delparent = True
894+ data = super(alt_osv, self).read(cr, uid, ids, fields=field_names, context=context, load=load)
895+ if (not data) or not context.get('recursion', True):
896+ if delparent:
897+ for d in data: del d['parent_id']
898+ return data
899+
900+ if not field_names:
901+ field_names = [ f for f in data[0] if f!='id' ]
902+ recursive = [ f for f in self._recursive if f in field_names ]
903+ explore = {}
904+ for d in data:
905+ if d['parent_id']:
906+ parent_id = (type(d['parent_id']) in (int,long)) and d['parent_id'] or d['parent_id'][0]
907+ explore[d['id']] = (parent_id, [ f for f in recursive if not d[f] ])
908+ if not explore[d['id']][1]: del explore[d['id']]
909+ if explore:
910+ exp = {}
911+ for data_id in explore:
912+ exp[data_id] = self.read(cr, uid, [explore[data_id][0]], explore[data_id][1], context=context, load=load)[0]
913+ del exp[data_id]['id']
914+ if 'parent_id' in exp[data_id]: del exp[data_id]['parent_id']
915+ for d in data:
916+ if d['id'] in exp:
917+ print exp[ d['id'] ]
918+ d.update( exp[ d['id'] ] )
919+ if delparent: del d['parent_id']
920+ return data
921
922=== added file 'product_structure/product.py'
923--- product_structure/product.py 1970-01-01 00:00:00 +0000
924+++ product_structure/product.py 2009-12-16 13:50:35 +0000
925@@ -0,0 +1,366 @@
926+# -*- coding: utf-8 -*-
927+
928+from osv import fields, osv
929+import alt_osv
930+from tools.translate import _
931+
932+
933+
934+PRODUCT_TYPES = [
935+ ('product', _('Stockable Product')),
936+ ('consu', _('Consumable')),
937+ ('service', _('Service')),
938+ ('fee', _('Fee')),
939+ ('recurrent', _('Recurrent')),
940+ ('recordable', _('Recordable Product')),
941+ ('hr', _('HR')) ]
942+
943+def categ_manager(categ):
944+ """Manager for a category (recursive function)
945+IN: browse on product.category
946+OUT: res.users id"""
947+ return (categ.user_id) and categ.user_id.id or (categ.parent_id) and categ_manager(categ.parent_id) or False
948+
949+def manager_categories(categ):
950+ """Categories for a manager
951+IN: browse on product.category
952+OUT: product.category list of ids"""
953+ category_ids= [ categ.id ]
954+ if categ.child_id:
955+ for c in categ.child_id:
956+ category_ids.extend( manager_categories(c) )
957+ return category_ids
958+
959+def type_categories(categ):
960+ """Children categories until type change
961+IN: browse on product.category
962+OUT: product.category list of ids"""
963+ category_ids= [ categ.id ]
964+ for c in categ.child_id:
965+ if not c.type_id:
966+ category_ids.extend( type_categories(c) )
967+ return category_ids
968+
969+
970+
971+class product_category_type(osv.osv):
972+ """Hierarchy levels on categories.
973+Assign one category type to a category, then all its children will have level type hierarchy depending on this category type, until hierarchy replaced by another hierarchy level type"""
974+ _name = "product.category.type"
975+ _description = "Product Category Type"
976+
977+ def _child_id( self, cr, uid, ids, name, args, context={} ):
978+ ret = {}
979+ for i in ids:
980+ ct= self.search( cr, uid, [('parent_id','=',i)] )
981+ ret[i]= (ct) and ct[0] or i
982+ return ret
983+
984+ _columns= {
985+ "name": fields.char("Name", required=True, size=64),
986+ "allow_products": fields.boolean("Product category", help="If not checked, all categories of this level won't be proposed to create products"),
987+ "parent_id": fields.many2one('product.category.type','Parent'),
988+ 'child_id': fields.function(_child_id, method=True, type='many2one', relation='product.category.type', obj='product.category.type', string='Child Type', readonly=True),
989+ }
990+ _defaults= {
991+ "allow_products": lambda *a: False,
992+ }
993+ _sql_constraints= [ ('unique_parent_id','unique (parent_id)','This parent is already used') ]
994+
995+product_category_type()
996+
997+
998+class product_category(alt_osv.alt_osv):
999+ _name = "product.category"
1000+ _inherit = "product.category"
1001+
1002+ def _type( self, cr, uid, ids, name, args, context={} ):
1003+ ret = {}
1004+ for b in self.browse(cr, uid, ids, context={'recursion':False}):
1005+ c= b
1006+ n=0
1007+ while c.parent_id and not c.type_id:
1008+ c=c.parent_id
1009+ n+=1
1010+ if c.type_id:
1011+ d=c.type_id
1012+ while n>0:
1013+ d=d.child_id
1014+ n=n-1
1015+ ret[b.id]= d.id
1016+ else: ret[b.id]= False
1017+ return ret
1018+
1019+ def _allow_products( self, cr, uid, ids, name, args, context={} ):
1020+ ret = {}
1021+ for b in self.browse(cr, uid, ids, context={'recursion':False}):
1022+ ret[b.id]= (b.relate_type_id) and b.relate_type_id.allow_products or False
1023+ return ret
1024+
1025+ def _parent_node( self, cr, uid, ids, name, args, context={} ):
1026+ ret = {}
1027+ for b in self.browse(cr, uid, ids, context={'recursion':False}):
1028+ c = b
1029+ while c.parent_id and not c.type_id: c=c.parent_id
1030+ ret[b.id] = c.id
1031+ return ret
1032+
1033+ def _partner_ids( self, cr, uid, ids, name, args, context={} ):
1034+ ret = {}
1035+ product_children = self.get_product_children(cr, uid, ids, context=context)
1036+ for cid in product_children:
1037+ if not product_children[cid]:
1038+ ret[cid] = []
1039+ continue
1040+ cr.execute("""SELECT s.name
1041+ FROM product_supplierinfo s, product_template t, product_product p
1042+ WHERE t.id=s.product_id AND p.product_tmpl_id=t.id AND p.active AND t.state not in ('obsolete','end') AND t.categ_id IN (%s)
1043+ GROUP BY 1""" % ','.join(map(str, product_children[cid])))
1044+ ret[cid]= (cr.rowcount) and [ r[0] for r in cr.fetchall() ] or []
1045+ return ret
1046+
1047+ def get_product_children(self, cr, uid, ids, context = None):
1048+ """Returns children categories in the same hierarchy that allows product.
1049+Add {'browse': True} in context, if you want browse objects instead of a list of ids"""
1050+ product_children = {}
1051+ res_browse = (context or {}).get('browse', False)
1052+ ctx = (context or {}).copy()
1053+ ctx.update({'recursion':False})
1054+ for b in self.browse(cr, uid, ids, context=ctx):
1055+ prods = []
1056+ a = [ b ]
1057+ while a:
1058+ r = []
1059+ for c in a:
1060+ if c.type_id and c.id!=b.id: break
1061+ if c.allow_products: prods.append((res_browse) and c or c.id)
1062+ r.extend( c.child_id )
1063+ a = r+[]
1064+ product_children[b.id] = prods
1065+ return product_children
1066+
1067+ _recursive = ['email','type_id','qty_alert_min','qty_alert_max','user_id','auto_picking','term_id','taxes_id','supplier_taxes_id','product_type']
1068+
1069+ _columns = {
1070+ 'name': fields.char('Name', size=64, required=True, translate=True, help="To reach a category, you can start with part of category type then ':'. You can also put '+' before a part of name to reach in parent categories. NB: searching products from category returns all children from categories found. ie: 've:ui++pa' returns categories (children on products search) with type containing 've', name containing 'ui', and parent name 2 levels over containing 'pa'"),
1071+ "email": fields.char('Email', size=128, help="Public email to contact someone responsible"),
1072+ "type_id": fields.many2one('product.category.type', 'Type', domain=[('parent_id','=',False)], help="If not empty, breaks hierarchy (accounting products may not have the same hierarchy as goods)"),
1073+ 'qty_alert_min': fields.integer('Qty alert (min)'),
1074+ 'qty_alert_max': fields.integer('Qty alert (max)'),
1075+ "user_id": fields.many2one('res.users','Manager'),
1076+ "auto_picking": fields.boolean("No remainder"),
1077+ "parent_node_id": fields.function(_parent_node, method=True, type='many2one', relation='product.category', obj='product.category', string='Type node', readonly=True, select=1),
1078+ "relate_type_id": fields.function(_type, method=True, type='many2one', relation='product.category.type', obj='product.category.type', string='Type (related)', readonly=True, select=1),
1079+ "allow_products": fields.function(_allow_products, method=True, type='boolean', string="Product category", readonly=True, select=1),
1080+ "partner_ids": fields.function(_partner_ids, method=True, type='one2many', relation='res.partner', string='Suppliers', select=1, readonly=True, help='Suppliers saling products from this category'),
1081+ "term_id": fields.many2one('account.payment.term','Payment term'),
1082+ 'taxes_id': fields.many2many('account.tax', 'product_taxes_rel',
1083+ 'prod_id', 'tax_id', 'Customer Taxes',
1084+ domain=[('parent_id','=',False),('type_tax_use','in',['sale','all'])], help="Taxes by default on products from this category"),
1085+ 'supplier_taxes_id': fields.many2many('account.tax',
1086+ 'product_supplier_taxes_rel', 'prod_id', 'tax_id',
1087+ 'Supplier Taxes', domain=[('parent_id', '=', False),('type_tax_use','in',['purchase','all'])], help="Supplier taxes by default on products from this category"),
1088+ 'product_type': fields.selection(PRODUCT_TYPES, 'Product Type',
1089+ help="Will change default on products for the way procurements are processed. Consumables are stockable products with infinite stock, or for use when you have no stock management in the system."),
1090+ }
1091+
1092+ def name_get(self, cr, uid, ids, context=None):
1093+ """Return hierarchy path from the first type node"""
1094+ ret = []
1095+ for b in self.browse(cr, uid, ids, context=context):
1096+ txts= []
1097+ a=b
1098+ while a.parent_id and not a.type_id:
1099+ a= a.parent_id
1100+ txts.append( a.name )
1101+ txts.reverse()
1102+ txt= ('»').decode('utf-8').join(txts)
1103+ txt= (txt) and b.name+' ('+txt+')' or b.name
1104+ ret.append( (b.id, txt) )
1105+ return ret
1106+
1107+ def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1108+ """Allows searching on function fields: user_id, relate_type_id, allow_products
1109+Allows expressions on searching category in hierarchy as PART-OF-TYPE:PART-OF-NAME+PART-OF-PARENT-NAME+..."""
1110+ rgs= []
1111+ for arg in args:
1112+ tids= []
1113+
1114+ if arg[0]=='user_id':
1115+ ids= self.search(cr, uid, [('user_id',arg[1],arg[2])], offset=offset, limit=limit, order=order, context=context, count=count)
1116+ if ids:
1117+ categ_ids= []
1118+ for b in self.browse(cr, uid, ids):
1119+ categ_ids.extend( manager_categories(b) )
1120+ rgs.append( ('id','in', categ_ids ) )
1121+ else: return []
1122+
1123+ elif arg[0]=='relate_type_id':
1124+ if type(arg[2]) in (int, long): tids= [arg[2]]
1125+ elif arg[1]=='in': tids= arg[2][0]
1126+ elif type(arg[2]) in (tuple, list): tids= [arg[2][0]]
1127+ else:
1128+ tids= self.pool.get('product.category.type').search(cr, uid, [('name',arg[1],arg[2])], context=context)
1129+
1130+ elif arg[0]=='allow_products':
1131+ tids= self.pool.get('product.category.type').search(cr, uid, [ arg ], context=context)
1132+
1133+ elif arg[0]=='name':
1134+ nam= arg[2]
1135+ v= [a.strip() for a in arg[2].split(':')][:2]
1136+ if len(v)==2:
1137+ typ, nam= v
1138+ args.append( ('relate_type_id', arg[1], typ) )
1139+ criterias= [a.strip() for a in nam.split('+')]
1140+ criteria= criterias.pop(0)
1141+ if criteria: rgs.append( ('name', arg[1], criteria) )
1142+ if criterias:
1143+ criteria= criterias.pop(0)
1144+ if criteria: rgs.append( ('parent_id', arg[1], criteria) )
1145+ n=1
1146+ while criterias:
1147+ criteria= criterias.pop(0)
1148+ if criteria:
1149+ ids= super(product_category, self).search(cr, uid, [ ('parent_id', arg[1], criteria) ], context=context)
1150+ cids= []
1151+ m=0
1152+ while m<n:
1153+ if not cids: cids= ids
1154+ cids= super(product_category, self).search(cr, uid, [ ('parent_id', 'in', cids) ], context=context)
1155+ m = (cids) and m+1 or n
1156+ if not cids: return cids
1157+ rgs.append( ('id', 'in', cids) )
1158+ n+=1
1159+
1160+ else: rgs.append(arg)
1161+
1162+ if tids:
1163+ reftypes= {}
1164+ for t in self.pool.get('product.category.type').browse(cr, uid, tids, context=context):
1165+ a= t
1166+ n= 0
1167+ while a.parent_id:
1168+ a = a.parent_id
1169+ n+=1
1170+ if not a.id in reftypes: reftypes[a.id]= {}
1171+ reftypes[a.id][t.id]= (n, t.child_id.id==t.id)
1172+ cids= super(product_category, self).search(cr, uid, [('type_id','in',reftypes.keys())], context=context)
1173+ categ_ids= []
1174+ for b in self.browse(cr, uid, cids, context=context):
1175+ for tid in reftypes[b.type_id.id]:
1176+ n= 0
1177+ c=[b]
1178+ while n<reftypes[b.type_id.id][tid][0]:
1179+ t= []
1180+ for a in c: t.extend(a.child_id)
1181+ c= t
1182+ n+=1
1183+ if reftypes[b.type_id.id][tid][1]:
1184+ for t in c: categ_ids.extend( type_categories(t) )
1185+ else: categ_ids.extend( [t.id for t in c ] )
1186+ rgs.append( ('id','in', categ_ids ) )
1187+
1188+ return super(product_category, self).search(cr, uid, rgs, offset=offset, limit=limit, order=order, context=context, count=count)
1189+
1190+product_category()
1191+
1192+
1193+
1194+class product_template( osv.osv ):
1195+ _name = "product.template"
1196+ _inherit = "product.template"
1197+
1198+ def _product_manager( self, cr, uid, ids, name, args, context={} ):
1199+ ret = {}
1200+ for b in self.browse(cr, uid, ids):
1201+ ret[b.id]=b.categ_id.user_id.id
1202+ return ret
1203+
1204+ # On change categ_id, inherits default values from category
1205+ # Right side argument, is category field name if different from product field name
1206+ _inherit_defaults = {
1207+ 'categ_id': [
1208+ ('product_manager', 'user_id'),
1209+ ('qty_alert_min',),
1210+ ('qty_alert_max',),
1211+ ('taxes_id',),
1212+ ('supplier_taxes_id',),
1213+ ('type', 'product_type') ],
1214+ }
1215+
1216+ _columns= {
1217+ "product_manager": fields.function(_product_manager, method=True, type='many2one', relation='res.users', string='Categ manager', select=1, readonly=True),
1218+ 'categ_id': fields.many2one('product.category','Category', domain=[('allow_products','=',True)], required=True, change_default=True, help="To reach a category, you can start with part of category type then ':'. You can also put '+' before a part of name to reach in parent categories. NB: searching products from category returns all children from categories found. ie: 've:ui++pa' returns categories (children on products search) with type containing 've', name containing 'ui', and parent name 2 levels over containing 'pa'"),
1219+ 'qty_alert_min': fields.float('Qty alert (min)', digits=(16, 3)),
1220+ 'qty_alert_max': fields.float('Qty alert (max)', digits=(16, 3)),
1221+ 'type': fields.selection(PRODUCT_TYPES, 'Product Type', required=True,
1222+ 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."),
1223+ }
1224+
1225+ def onchange_categ_id(self, cr, uid, ids, ref_id, context=None):
1226+ ret = {'value': {} }
1227+# for f in self._inherit_defaults:
1228+# if isinstance(self._columns[f[0]], fields.one2many) or isinstance(self._columns[f[0]], fields.many2many):
1229+# ret['value'][f[0]] = []
1230+ if not ref_id: return ret
1231+ obj = self._columns['categ_id']._obj
1232+ res = self.pool.get(obj).browse(cr, uid, ref_id, context=context)
1233+ for f in self._inherit_defaults['categ_id']:
1234+ val = eval("res."+((len(f)==1) and f[0] or f[1]))
1235+ if isinstance(val, osv.orm.browse_null):
1236+ ret['value'][f[0]] = False
1237+ continue
1238+ if isinstance(self._columns[f[0]], fields.one2many) or isinstance(self._columns[f[0]], fields.many2many):
1239+ ret['value'][f[0]] = [v.id for v in val]
1240+ elif isinstance(self._columns[f[0]], fields.many2one):
1241+ ret['value'][f[0]] = val.id
1242+ else:
1243+ ret['value'][f[0]] = val
1244+ return ret
1245+
1246+product_template()
1247+
1248+
1249+
1250+def compare_suppliers(a,b):
1251+ if not a[2]: return 1
1252+ if not b[2]: return -1
1253+ if not a[2]['action']=='none': return 1
1254+ if not a[2]['action']=='create': return -1
1255+ return (b[2]['action']=='create') and 1 or -1
1256+
1257+class product_product( osv.osv ):
1258+ _name = "product.product"
1259+ _inherit = "product.product"
1260+
1261+ def onchange_categ_id(self, cr, uid, ids, categ_id):
1262+ if not ids: return self.pool.get('product.template').onchange_categ_id(cr, uid, [], categ_id)
1263+ pids = [prod['product_tmpl_id'][0] for prod in self.read(cr, uid, ids, ['product_tmpl_id'])]
1264+ return self.pool.get('product.template').onchange_categ_id(cr, uid, pids, categ_id)
1265+
1266+ def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1267+ rargs=[]
1268+ for arg in args:
1269+ cids=[]
1270+ if arg[0]=='categ_id':
1271+ if type(arg[2]) in (int, long): cids= [arg[2]]
1272+ elif arg[1]=='in': cids= arg[2]
1273+ elif type(arg[2]) in (list, tuple): cids= [arg[2][0]]
1274+ else:
1275+ cids= self.pool.get("product.category").search(cr,uid,[('name',arg[1],arg[2])])
1276+
1277+ # LIGNES À RETIRER (conservées par acquis de conscience):
1278+ #for b in self.pool.get("product.category").browse(cr, uid, cat_ids, context=context):
1279+ #cids.extend( manager_categories(b) )
1280+
1281+ if arg[0]=='product_manager':
1282+ cids= self.pool.get("product.category").search(cr,uid,[('user_id',arg[1], arg[2])])
1283+
1284+ if cids:
1285+ rargs.append( ('categ_id','in',cids) )
1286+ elif arg[0] in ('categ_id', 'product_manager'): return []
1287+
1288+ if not (arg[0] in ('categ_id','product_manager') or (arg[0]=='seller_ids' and type(arg[2]) in (int,long))): rargs.append(arg)
1289+ return super(product_product, self).search(cr, uid,rargs, offset=offset, limit=limit, order=order, context=context, count=count)
1290+
1291+product_product()
1292
1293=== added file 'product_structure/product_view.xml'
1294--- product_structure/product_view.xml 1970-01-01 00:00:00 +0000
1295+++ product_structure/product_view.xml 2009-12-16 13:50:35 +0000
1296@@ -0,0 +1,145 @@
1297+<?xml version="1.0" ?>
1298+<openerp>
1299+ <data>
1300+
1301+ <record id="product_category_type_form_view" model="ir.ui.view">
1302+ <field name="name">product.category.type.form</field>
1303+ <field name="model">product.category.type</field>
1304+ <field name="type">form</field>
1305+ <field name="arch" type="xml">
1306+ <form string="Category type">
1307+ <field name="name"/>
1308+ <field name="allow_products"/>
1309+ <field name="parent_id"/>
1310+ <field name="child_id"/>
1311+ </form>
1312+ </field>
1313+ </record>
1314+
1315+ <record id="product_category_type_tree_view" model="ir.ui.view">
1316+ <field name="name">product.category.type.tree</field>
1317+ <field name="model">product.category.type</field>
1318+ <field name="type">tree</field>
1319+ <field name="arch" type="xml">
1320+ <tree string="Category type">
1321+ <field name="parent_id"/>
1322+ <field name="name"/>
1323+ <field name="child_id"/>
1324+ </tree>
1325+ </field>
1326+ </record>
1327+
1328+ <record id="product_category_type_action" model="ir.actions.act_window">
1329+ <field name="name">Category types</field>
1330+ <field name="type">ir.actions.act_window</field>
1331+ <field name="res_model">product.category.type</field>
1332+ <field name="view_type">form</field>
1333+ <field name="view_id" ref="product_category_type_tree_view"/>
1334+ </record>
1335+
1336+ <menuitem action="product_category_type_action" id="menu_product_category_type" parent="product.menu_config_product" groups="product.group_product_manager"/>
1337+
1338+ <record id="product_category_list_view2" model="ir.ui.view">
1339+ <field name="name">product.category.list</field>
1340+ <field name="model">product.category</field>
1341+ <field name="type">tree</field>
1342+ <field name="priority">0</field>
1343+ <field name="arch" type="xml">
1344+ <tree string="Product Categories">
1345+ <field name="name"/>
1346+ <field name="parent_node_id"/>
1347+ <field name="allow_products"/>
1348+ <field name="parent_id"/>
1349+ </tree>
1350+ </field>
1351+ </record>
1352+
1353+ <record id="product.product_category_action_form" model="ir.actions.act_window">
1354+ <field name="name">Products Categories</field>
1355+ <field name="type">ir.actions.act_window</field>
1356+ <field name="res_model">product.category</field>
1357+ <field name="view_type">form</field>
1358+ <field name="view_id" ref="product_category_list_view2"/>
1359+ </record>
1360+
1361+ <record id="product_category_form_view1" model="ir.ui.view">
1362+ <field name="name">product.category.form</field>
1363+ <field name="model">product.category</field>
1364+ <field name="inherit_id" ref="product.product_category_form_view"/>
1365+ <field name="type">form</field>
1366+ <field name="arch" type="xml">
1367+ <field name="sequence" position="after">
1368+ <field name="parent_node_id"/>
1369+ <separator colspan="4" string="Products configuration"/>
1370+ <group colspan="2" col="3">
1371+ <field name="type_id"/>
1372+ <field name="user_id"/>
1373+ <field name="email"/>
1374+ <field name="term_id"/>
1375+ </group>
1376+ <group colspan="2" col="3">
1377+ <field name="parent_node_id" colspan="3"/>
1378+ <field name="qty_alert_min" string="Qty alert (min/max)"/>
1379+ <field name="qty_alert_max" nolabel="1"/>
1380+ <field name="allow_products"/>
1381+ <label string=" " colspan="1"/>
1382+ </group>
1383+ <separator colspan="2" string="Supply taxes (default for products)"/>
1384+ <separator colspan="2" string="Sale taxes (default for products)"/>
1385+ <field name="supplier_taxes_id" nolabel="1" colspan="2"/>
1386+ <field name="taxes_id" nolabel="1" colspan="2"/>
1387+ <field name="partner_ids" colspan="4" nolabel="1"/>
1388+ </field>
1389+ </field>
1390+ </record>
1391+
1392+ <record id="product.product_category_action_form" model="ir.actions.act_window">
1393+ <field name="name">Products Categories</field>
1394+ <field name="type">ir.actions.act_window</field>
1395+ <field name="res_model">product.category</field>
1396+ <field name="context">{'recursion':False}</field>
1397+ <field name="view_type">form</field>
1398+ <field name="view_id" ref="product.product_category_list_view"/>
1399+ </record>
1400+
1401+ <record id="product_normal_form_view1" model="ir.ui.view">
1402+ <field name="name">product.normal.form</field>
1403+ <field name="model">product.product</field>
1404+ <field name="inherit_id" ref="product.product_normal_form_view"/>
1405+ <field name="type">form</field>
1406+ <field name="arch" type="xml">
1407+ <field name="categ_id" position="replace">
1408+ <field name="categ_id" on_change="onchange_categ_id(categ_id)"/>
1409+ </field>
1410+ </field>
1411+ </record>
1412+
1413+ <record id="product_normal_form_view2" model="ir.ui.view">
1414+ <field name="name">product.normal.form</field>
1415+ <field name="model">product.product</field>
1416+ <field name="inherit_id" ref="product.product_normal_form_view"/>
1417+ <field name="type">form</field>
1418+ <field name="arch" type="xml">
1419+ <field name="product_manager" position="replace">
1420+ <group colspan="2" col="3">
1421+ <field name="qty_alert_min" string="Qty alert (min/max)"/>
1422+ <field name="qty_alert_max" nolabel="1"/>
1423+ </group>
1424+ </field>
1425+ </field>
1426+ </record>
1427+
1428+ <record id="product_normal_form_view3" model="ir.ui.view">
1429+ <field name="name">product.normal.form</field>
1430+ <field name="model">product.product</field>
1431+ <field name="inherit_id" ref="stock.view_normal_stock_property_form"/>
1432+ <field name="type">form</field>
1433+ <field name="arch" type="xml">
1434+ <field name="virtual_available" position="after">
1435+ <field name="product_manager"/>
1436+ </field>
1437+ </field>
1438+ </record>
1439+
1440+ </data>
1441+</openerp>
1442
1443=== added file 'product_structure/purchase.py'
1444--- product_structure/purchase.py 1970-01-01 00:00:00 +0000
1445+++ product_structure/purchase.py 2009-12-16 13:50:35 +0000
1446@@ -0,0 +1,27 @@
1447+# -*- coding: utf-8 -*-
1448+
1449+from osv import fields, osv
1450+
1451+
1452+
1453+class purchase_order(osv.osv):
1454+ _name = 'purchase.order'
1455+ _inherit = 'purchase.order'
1456+
1457+ _columns= {
1458+ 'categ_id': fields.many2one('product.category','Category', change_default=True),
1459+ }
1460+
1461+ def onchange_partner_id(self, cr, uid, ids, partner_id):
1462+ ret = super( purchase_order, self ).onchange_partner_id(cr, uid, ids, partner_id)
1463+ if not partner_id: return ret
1464+
1465+ categs = self.pool.get("res.partner").read(cr, uid, [ partner_id ], ['categ_ids'])
1466+ if not categs: return ret
1467+
1468+ ret['domain'] = ret.get('domain', {})
1469+ ret['domain']['categ_id']= [('id', 'in', categs[0]['categ_ids'])]
1470+ if len(categs[0]['categ_ids']) == 1: ret['value']['categ_id'] = categs[0]['categ_ids'][0]
1471+ return ret
1472+
1473+purchase_order()
1474
1475=== added file 'product_structure/purchase_view.xml'
1476--- product_structure/purchase_view.xml 1970-01-01 00:00:00 +0000
1477+++ product_structure/purchase_view.xml 2009-12-16 13:50:35 +0000
1478@@ -0,0 +1,30 @@
1479+<?xml version="1.0" ?>
1480+<openerp>
1481+ <data>
1482+
1483+ <record id="purchase_order_form" model="ir.ui.view">
1484+ <field name="name">purchase.order.form</field>
1485+ <field name="model">purchase.order</field>
1486+ <field name="type">form</field>
1487+ <field name="inherit_id" ref="purchase.purchase_order_form"/>
1488+ <field name="arch" type="xml">
1489+ <field name="shipped" position="after">
1490+ <field name="categ_id" on_change="categ_id_change(categ_id,context)"/>
1491+ </field>
1492+ </field>
1493+ </record>
1494+
1495+ <record id="purchase_order_line_form" model="ir.ui.view">
1496+ <field name="name">purchase.order.line.form</field>
1497+ <field name="model">purchase.order.line</field>
1498+ <field name="type">form</field>
1499+ <field name="inherit_id" ref="purchase.purchase_order_line_form"/>
1500+ <field name="arch" type="xml">
1501+ <field name="product_id" position="replace">
1502+ <field colspan="4" context="partner_id=parent.partner_id,quantity=product_qty,pricelist=parent.pricelist_id,uom=product_uom,warehouse=parent.warehouse_id" name="product_id" on_change="product_id_change(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order, parent.fiscal_position)" domain="[('seller_ids', 'in', (parent.partner_id,False)),('categ_id', '=', parent.categ_id)]"/>
1503+ </field>
1504+ </field>
1505+ </record>
1506+
1507+ </data>
1508+</openerp>
1509
1510=== added file 'product_structure/res.py'
1511--- product_structure/res.py 1970-01-01 00:00:00 +0000
1512+++ product_structure/res.py 2009-12-16 13:50:35 +0000
1513@@ -0,0 +1,47 @@
1514+# -*- coding: utf-8 -*-
1515+
1516+from osv import fields, osv
1517+
1518+
1519+
1520+class res_partner(osv.osv):
1521+ _name = "res.partner"
1522+ _inherit = "res.partner"
1523+
1524+ def _categ_ids( self, cr, uid, ids, name, args, context={} ):
1525+ ret = {}
1526+ for i in ids: ret[i] = []
1527+ cids = self.pool.get("product.category").search(cr, uid, [("type_id","=","")], context={'recursion':False})
1528+ for r in self.pool.get("product.category").read(cr, uid, cids, ["partner_ids"]):
1529+ for p in r['partner_ids']:
1530+ if p in ret: ret[p].append( r['id'] )
1531+ return ret
1532+
1533+ _columns = {
1534+ "categ_ids": fields.function(_categ_ids, method=True, type='one2many', relation='product.category', string='Categorie', select=1, readonly=True),
1535+ "product_ids": fields.many2many('product.product', 'product_supplierinfo', 'name', 'product_id', 'Products', readonly=True),
1536+ }
1537+
1538+res_partner()
1539+
1540+
1541+
1542+class res_users( osv.osv ):
1543+ _name = 'res.users'
1544+ _inherit = 'res.users'
1545+
1546+ _columns = {
1547+ 'context_categ_id': fields.many2one('product.category','Category'),
1548+ }
1549+
1550+ def context_get(self, cr, uid, context=None):
1551+ user = self.browse(cr, uid, uid, context)
1552+ result = super(res_users, self).context_get(cr, uid, context=context)
1553+ for k in self._columns.keys():
1554+ if k.startswith('context_'):
1555+ result[k[8:]] = ( isinstance(getattr(user, k), osv.orm.browse_record) and not isinstance(getattr(user, k), osv.orm.browse_null) ) \
1556+ and getattr(user,k).id \
1557+ or getattr(user,k)
1558+ return result
1559+
1560+res_users()
1561
1562=== added file 'product_structure/res_view.xml'
1563--- product_structure/res_view.xml 1970-01-01 00:00:00 +0000
1564+++ product_structure/res_view.xml 2009-12-16 13:50:35 +0000
1565@@ -0,0 +1,35 @@
1566+<?xml version="1.0" ?>
1567+<openerp>
1568+ <data>
1569+
1570+ <record id="view_partner_form" model="ir.ui.view">
1571+ <field name="name">res.partner.form</field>
1572+ <field name="model">res.partner</field>
1573+ <field name="type">form</field>
1574+ <field name="inherit_id" ref="base.view_partner_form"/>
1575+ <field name="arch" type="xml">
1576+ <notebook position="inside">
1577+ <page string="Products">
1578+ <separator string="Products" colspan="3"/>
1579+ <separator string="Products categories" colspan="1"/>
1580+ <field name="product_ids" nolabel="1" colspan="3"/>
1581+ <field name="categ_ids" nolabel="1" colspan="1"/>
1582+ </page>
1583+ </notebook>
1584+ </field>
1585+ </record>
1586+
1587+ <record id="view_users_form_simple_modif" model="ir.ui.view">
1588+ <field name="name">res.users.form.modif</field>
1589+ <field name="model">res.users</field>
1590+ <field name="type">form</field>
1591+ <field name="inherit_id" ref="base.view_users_form_simple_modif"/>
1592+ <field name="arch" type="xml">
1593+ <field name="signature" position="after">
1594+ <field name="context_categ_id"/>
1595+ </field>
1596+ </field>
1597+ </record>
1598+
1599+ </data>
1600+</openerp>

Subscribers

People subscribed via source and target branches