Merge lp:~frederic-declercq/openobject-addons/addons-fu into lp:openobject-addons/extra-trunk
- addons-fu
- Merge into trunk-extra-addons
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 |
Related bugs: |
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.
Commit message
Description of the change
Raphaël Valyi - http://www.akretion.com (rvalyi) wrote : Posted in a previous version of this proposal | # |
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/
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.
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,
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://
(Not sure this is efficient enough though)
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.
NB - SQL:
To avoid SQL, I used:
vals[
if lp:~frederic-declercq/openobject-server/server-fu-reference_browse is refused, this should be:
vals[
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
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 | |
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> |
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.