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
=== added directory 'network_interactivity'
=== added file 'network_interactivity/__init__.py'
--- network_interactivity/__init__.py 1970-01-01 00:00:00 +0000
+++ network_interactivity/__init__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,5 @@
1# -*- coding: utf-8 -*-
2
3import netservice
4import network
5import res
0\ No newline at end of file6\ No newline at end of file
17
=== added file 'network_interactivity/__terp__.py'
--- network_interactivity/__terp__.py 1970-01-01 00:00:00 +0000
+++ network_interactivity/__terp__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,35 @@
1# -*- coding: utf-8 -*-
2
3{
4 "name" : "Network interactivity",
5 "description" :
6 """
7Adds actions, online/offline, active, availability to network materials
8View only hardware stations. Components are listed inside (both are network.material objects)
9Searching on IP address returns material, not component
10
11New netsvc 'network' connection to log in from IP address returning user linked the material type linked to this IP address
12IP address, and station used in context (res.users context_get() overwrite)
13
14NB: Waiting for merge on lp:~frederic-declercq/openobject-server/server-fu-ip_address to get IP address
15__________________________________________
16=> descriptions & screenshots:
17 http://www.lafermedusart.com/modules/network_interactivity.html
18
19All modules from Fermes Urbaines are put on launchpad when used on production.
20We just offer support to Fermes Urbaines partners.
21Eventhought, all addons are updated as soon as tested.
22""",
23 "version" : "1.0.0",
24 "depends" : [ "base", "network" ],
25 "author" : "Fermes Urbaines",
26 "website" : "http://www.lafermedusart.com/modules/network_interactivity.html",
27 "category" : "Generic Modules/Interfaces",
28 "init_xml" : [ ],
29 "demo_xml" : [ ],
30 "update_xml" : [
31 'network_view.xml',
32 ],
33 "active": True,
34 "installable" : True
35}
036
=== added file 'network_interactivity/netservice.py'
--- network_interactivity/netservice.py 1970-01-01 00:00:00 +0000
+++ network_interactivity/netservice.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,36 @@
1# -*- coding: utf-8 -*-
2import netsvc, sql_db
3
4class network_interface(netsvc.Service):
5
6 def __init__(self,name="network"):
7 netsvc.Service.__init__(self,name)
8 self.joinGroup("web-services")
9 self.exportMethod(self.ip_login)
10 self.exportMethod(self.code39_login)
11
12 def ip_login(self, ip_address, dbname):
13 # IP address is provided by querying. Only DB name required for hardware identified on network.material with user_id set on material type
14 logger = netsvc.Logger()
15 if not (dbname and ip_address): return False
16 ip_ok = True
17 try:
18 for n in map(int,ip_address.split('.')):
19 if n<0 or n>254: ip_ok = False
20 except: ip_ok = False
21 if not ip_ok:
22 logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: %s is not a valid IP address' % ip_address)
23 return False
24 cr= sql_db.db_connect(dbname).cursor()
25 cr.execute("""SELECT u.id, u.password
26 FROM network_material m, network_hardware_type t, res_users u
27 WHERE t.id=m.type AND u.id=t.user_id AND m.ip_addr='%s'""" % ip_address)
28 if not cr.rowcount:
29 logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: Access denied for %s' % ip_address)
30 return False
31 logger.notifyChannel("web-service", netsvc.LOG_INFO, 'IP login: Successful login for %s' % ip_address)
32 uid, password= cr.fetchone()
33 cr.close()
34 return uid, password
35
36network_interface()
037
=== added file 'network_interactivity/network.py'
--- network_interactivity/network.py 1970-01-01 00:00:00 +0000
+++ network_interactivity/network.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,118 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4import time
5
6class network_hardware_type(osv.osv):
7 _name = "network.hardware.type"
8 _inherit = "network.hardware.type"
9
10 _columns = {
11 '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"),
12 }
13
14network_hardware_type()
15
16class network_network(osv.osv):
17 _name = 'network.network'
18 _inherit = 'network.network'
19
20 _columns = {
21 'active': fields.boolean('Active'),
22 }
23
24 _defaults = {
25 'active': lambda *a: True,
26 }
27
28network_network()
29
30class network_material(osv.osv):
31 _name = "network.material"
32 _inherit = "network.material"
33
34 _columns = {
35 'active': fields.boolean('Active'),
36 'online': fields.boolean('On line'),
37 'available': fields.boolean('Available'),
38 'action_ids': fields.one2many('network.material.action', 'material_id', 'Actions', domain=[('date','>=',time.strftime('%Y-%m-%d 00:00:00'))]),
39 }
40
41 _defaults = {
42 'online': lambda *a: True,
43 'active': lambda *a: True,
44 'available': lambda *a: True,
45 }
46
47 def check_busy(self, cr, uid, ids, context=None):
48 ret = {}
49 if not ids: return ret
50 for i in ids: ret[i] = False
51 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)))
52 if cr.rowcount:
53 for r in cr.fetchall(): ret[r[0]] = r[1]
54 avail_ids = [ i for i in ret if not ret[i] ]
55 if avail_ids:
56 self.pool.get('network.material').write(cr, uid, avail_ids, {'available': True})
57 return ret
58
59 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
60 # on IP search, return computers, not components
61 rargs = []
62 for arg in args:
63 if arg[0] == 'ip_addr':
64 component_ids = super(network_material, self).search(cr, uid, [arg], offset=offset, limit=limit, order=order, context=context, count=count)
65 computer_ids = []
66 for material in self.browse(cr, uid, component_ids, context=context):
67 while material.parent_id: material = material.parent_id
68 computer_ids.append( material.id )
69 rargs.append( ('id', 'in', list(set( computer_ids ))) )
70 else: rargs.append( arg )
71 args = rargs
72
73 # view just computers, not components
74 if not 'parent_id' in [arg[0] for arg in args]:
75 args.append( ('parent_id', '=', False) )
76 return super(network_material, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
77
78network_material()
79
80def _action_models_get(obj, cr, uid, context={}):
81 mids = obj.pool.get('ir.model').search(cr, uid, [('model','in', obj._ref_tables)])
82 return [ (r['model'],r['name']) for r in obj.pool.get('ir.model').read(cr, uid, mids, ['model','name'], context=context) ]
83
84class network_material_action(osv.osv):
85 _name = 'network.material.action'
86 _description = 'Actions done on hardware'
87
88 _ref_tables = [ 'pos.order', 'sale.order', 'stock.inventory', 'stock.picking', 'purchase.order', 'account.invoice' ]
89
90 _columns = {
91 'name': fields.char('Action', size=64, required=True),
92 'ref': fields.reference('Reference', selection=_action_models_get, size=128),
93 'material_id': fields.many2one('network.material', 'Material', required=True),
94 'date': fields.datetime('Started at', required=True),
95 'date_done': fields.datetime('Done at'),
96 }
97
98 _order = 'date desc'
99
100 _defaults = {
101 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
102 }
103
104 def unlink(self, cr, uid, ids, context=None):
105 if not ids: return True
106 self.write(cr, uid, ids, {'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
107 mids = [ r['material_id'][0] for r in self.read(cr, uid, ids, ['material_id']) ]
108 self.pool.get('network.material').check_busy(cr, uid, mids, context=context)
109 return True
110
111 def create(self, cr, uid, vals, context=None):
112 if not 'date' in vals: vals['date'] = time.strftime('%Y-%m-%d %H:%M:%S')
113 if not 'name' in vals: vals['name'] = vals['ref']
114 ret = super(network_material_actions, self).create(cr, uid, vals, context=context)
115 self.pool.get('network.material').write(cr, uid, [ vals['material_id'], ], {'available': False})
116 return ret
117
118network_material_action()
0119
=== added file 'network_interactivity/network_view.xml'
--- network_interactivity/network_view.xml 1970-01-01 00:00:00 +0000
+++ network_interactivity/network_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,113 @@
1<?xml version="1.0"?>
2<openerp>
3 <data>
4
5 <record model="ir.ui.view" id="edit_network2">
6 <field name="name">network.material.form</field>
7 <field name="model">network.material</field>
8 <field name="type">form</field>
9 <field name="inherit_id" ref="network.edit_network"/>
10 <field name="arch" type="xml">
11 <notebook position="inside">
12 <page string="Elements">
13 <field name="child_id" nolabel="1" colspan="4"/>
14 </page>
15 <page string="Actions">
16 <field name="online" select="1"/>
17 <field name="available" select="1"/>
18 <field name="action_ids" nolabel="1" colspan="4"/>
19 </page>
20 </notebook>
21 </field>
22 </record>
23
24 <record model="ir.ui.view" id="edit_network3">
25 <field name="name">network.material.form</field>
26 <field name="model">network.material</field>
27 <field name="type">form</field>
28 <field name="inherit_id" ref="network.edit_network"/>
29 <field name="arch" type="xml">
30 <field name="network_id" position="after">
31 <field name="active" select="1"/>
32 </field>
33 </field>
34 </record>
35
36
37 <record model="ir.ui.view" id="material_view">
38 <field name="name">network.material.tree</field>
39 <field name="model">network.material</field>
40 <field name="priority" eval="1"/>
41 <field name="type">tree</field>
42 <field name="field_parent">child_id</field>
43 <field name="arch" type="xml">
44 <tree string="Network Material">
45 <field name="name"/>
46 <field name="ip_addr"/>
47 <field name="active" />
48 <field name="online"/>
49 <field name="available" />
50 </tree>
51 </field>
52 </record>
53
54
55
56 <record model="ir.ui.view" id="view_hardware_type_form">
57 <field name="name">network.hardware.type.form</field>
58 <field name="model">network.hardware.type</field>
59 <field name="type">form</field>
60 <field name="inherit_id" ref="network.view_hardware_type_form"/>
61 <field name="arch" type="xml">
62 <field name="networkable" position="after">
63 <field name="user_id" select="1"/>
64 </field>
65 </field>
66 </record>
67
68 </data>
69 <data noupdate="1">
70
71 <record forcecreate="True" id="server_type" model="network.hardware.type">
72 <field name="name">Server</field>
73 <field name="networkable">False</field>
74 </record>
75 <record forcecreate="True" id="client_type" model="network.hardware.type">
76 <field name="name">Client computer</field>
77 <field name="networkable">False</field>
78 </record>
79 <record forcecreate="True" id="eth_type" model="network.hardware.type">
80 <field name="name">Ethernet</field>
81 <field name="networkable">True</field>
82 </record>
83 <record forcecreate="True" id="lo_type" model="network.hardware.type">
84 <field name="name">Loopback</field>
85 <field name="networkable">True</field>
86 <field name="user_id">1</field>
87 </record>
88 <record forcecreate="True" id="wifi_type" model="network.hardware.type">
89 <field name="name">WiFi</field>
90 <field name="networkable">True</field>
91 </record>
92
93 <record forcecreate="True" id="localhost" model="network.network">
94 <field name="name">localhost</field>
95 <field name="range">127.0.0</field>
96 <field name="contact_id" ref="base.main_address"/>
97 </record>
98
99 <record forcecreate="True" id="self_server" model="network.material">
100 <field name="name">Main server</field>
101 <field name="user_id">Main server</field>
102 <field name="type" ref="server_type"/>
103 </record>
104 <record forcecreate="True" id="self_server_ip" model="network.material">
105 <field name="name">Main server loopback</field>
106 <field name="ip_addr">127.0.0.1</field>
107 <field name="type" ref="lo_type"/>
108 <field name="network_id" ref="localhost"/>
109 <field name="parent_id" ref="self_server"/>
110 </record>
111
112 </data>
113</openerp>
0114
=== added file 'network_interactivity/res.py'
--- network_interactivity/res.py 1970-01-01 00:00:00 +0000
+++ network_interactivity/res.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,18 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4
5class res_users( osv.osv ):
6 _name = 'res.users'
7 _inherit = 'res.users'
8
9 def context_get(self, cr, uid, context=None):
10 ret = super(res_users, self).context_get(cr, uid, context=context)
11 if not (context or {}).get('ip_address', False): return ret
12 ret['ip_address'] = context['ip_address']
13 material_ids = self.pool.get('network.material').search(cr, uid, [('ip_addr','=',context['ip_address'])])
14 if not material_ids: return ret
15 ret['material_id'] = material_ids[0]
16 return ret
17
18res_users()
019
=== added directory 'product_multibarcode'
=== added file 'product_multibarcode/__init__.py'
--- product_multibarcode/__init__.py 1970-01-01 00:00:00 +0000
+++ product_multibarcode/__init__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,4 @@
1# -*- coding: utf-8 -*-
2
3import res
4import product
0\ No newline at end of file5\ No newline at end of file
16
=== added file 'product_multibarcode/__terp__.py'
--- product_multibarcode/__terp__.py 1970-01-01 00:00:00 +0000
+++ product_multibarcode/__terp__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,37 @@
1# -*- coding: utf-8 -*-
2
3{
4 "name" : "Product ean8 & 13 barcodes with weight",
5 "description" :
6 """
7- Weight barcode depending on currency (French FRF, Euro...) or weight include in barcode
8- Price calculation
9- Ean automated creation
10- Key calculation
11- Many barcodes depending on packaging (ie: 1 lot of 6 milk packs)
12- ean13 field as a function for compatibility
13- ean13 search on product fields
14- methods to get quantity / price (ie: for use with POS)
15- ean13.ttf encoding
16__________________________________________
17=> descriptions & screenshots:
18 http://www.fermes-urbaines.com/openerp/modules/product_multibarcode.html
19
20All modules from Fermes Urbaines are put on launchpad when used on production.
21We just offer support to Fermes Urbaines partners.
22Eventhought, all addons are updated as soon as tested.
23""",
24 "version" : "1.0.0",
25 "depends" : [ "product_tax_incl" ],
26 "author" : "Fermes Urbaines",
27 "website" : "http://www.fermes-urbaines.com/openerp/modules/product_multibarcode.html",
28 "category" : "Generic Modules/Base",
29 "init_xml" : [ ],
30 "demo_xml" : [ ],
31 "update_xml" : [
32 "res_view.xml",
33 "product_view.xml"
34 ],
35 "active": True,
36 "installable" : True
37}
038
=== added file 'product_multibarcode/product.py'
--- product_multibarcode/product.py 1970-01-01 00:00:00 +0000
+++ product_multibarcode/product.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,298 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4import re
5
6# ean.ttf translation tables:
7TTF_EANTABLES=[
8 ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'),
9 ('K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'),
10 ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j')]
11TTF_EAN6=['000000', '001011', '001101', '001110', '010011', '011001', '011100', '010101', '010110', '011010']
12
13class product_packaging( osv.osv ):
14 _name = "product.packaging"
15 _inherit = "product.packaging"
16
17 # check if unique (weight/price + unit)
18 def _unique_ean(self, cr, uid, ids):
19 for b in self.browse(cr,uid,ids):
20 if b.ean and len(self.search(cr, uid, [('ean','=',b.ean)]))>1: return False
21 return True
22
23 # check EAN syntax
24 def _check_ean(self, cr, uid, ids):
25 for b in self.browse(cr, uid, ids):
26 if b.ean:
27 if re.sub('[0-9]','',b.ean) or (len(b.ean)<7) or (len(b.ean)<12 and not b.encoding):
28 return False
29 return True
30
31 # ean.ttf translation: use this to write barcode with this font
32 def _ttf(self, cr, uid, ids, name, arg, context=None):
33 ret= {}
34 for b in self.browse(cr, uid, ids):
35 if b.ean:
36 tb=TTF_EAN6[int(b.ean[:1])].replace('A','1').replace('B','2')
37 txt=b.ean[:1]
38 i=1
39 while i<7:
40 txt+=TTF_EANTABLES[int(tb[i-1:i])][int(b.ean[i:i+1])]
41 i+=1
42 txt+='*'
43 while i<13:
44 txt+=TTF_EANTABLES[2][int(b.ean[i:i+1])]
45 i+=1
46 ret[b.id]=txt+'+'
47 else:
48 ret[b.id]=''
49 return ret
50
51 _columns= {
52 "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)"),
53 "ttf": fields.function(_ttf, method=True, type='char', size=20, string='TTF', readonly=True, store=True, help="Encryption to use with ean3.ttf")
54 }
55 _defaults= {
56 "ean": lambda obj,cr,uid,context={}: obj.new_ean( cr, uid, [], encoding=False),
57 }
58 _constraints= [
59 (_check_ean, 'This barcode is not EAN8 or EAN13', ['ean']),
60 (_unique_ean, 'This barcode is already used', ['ean','product_id','encoding'])
61 ]
62
63 def onchange_encoding(self, cr, uid, ids, encoding):
64 return {'values': {'ean': self.new_ean( cr, uid, ids, encoding=encoding)} }
65
66 # creates an internal barcode (starting with 2) reaching first available, caring if weighted
67 # 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
68 def new_ean( self, cr, uid, ids, encoding=False):
69 if encoding:
70 cr.execute("""SELECT e1.ean
71 FROM (SELECT (LPAD(ean,7)::bigint+1)::varchar||'00000' AS ean FROM product_packaging WHERE ean ILIKE '2%%'
72 UNION SELECT '200000000000' AS ean) e1
73 LEFT JOIN product_packaging e2 ON LPAD(e2.ean,7)=LPAD(e1.ean,7)
74 WHERE e2.ean IS NULL
75 ORDER BY 1
76 LIMIT 1""")
77 else:
78 cr.execute("""SELECT e1.ean
79 FROM (SELECT (LPAD(ean,12)::bigint+1)::varchar AS ean FROM product_packaging WHERE ean ILIKE '2%%'
80 UNION SELECT (LPAD(ean,7)::bigint+1)::varchar||'00000' AS ean FROM product_packaging WHERE ean ILIKE '2%%'
81 UNION SELECT '200000000000' AS ean) e1
82 LEFT JOIN product_packaging e2 ON LPAD(e2.ean,12)=e1.ean AND e2.encoding IS NULL
83 LEFT JOIN product_packaging e3 ON LPAD(e3.ean,7)=LPAD(e1.ean,7) AND e3.encoding IS NOT NULL
84 WHERE e2.ean IS NULL AND e3.ean IS NULL
85 ORDER BY 1
86 LIMIT 1""")
87 return self.ean_key(cr, uid, str(cr.fetchone()[0]))
88
89 # builds EAN13 key
90 def ean_key(self, cr, uid, ean):
91 ean= ean[:12]
92 n=0
93 i=0
94 while i<len(ean):
95 n+= int(ean[i:i+1])
96 i+=2
97 i=1
98 while i<len(ean):
99 n+= int(ean[i:i+1])*3
100 i+=2
101 key= 10- (n % 10)
102 if key==10: key=0
103 return ean+str(key)
104
105 # reaches weight encoding (default on res_company) if not set
106 # removes encoding if no decimal unit set (depending on rounding of uos_id / uom_id)
107 # sets key (ignore if allready set)
108 def create(self, cr, uid, vals, context=None):
109 e= vals.get('ean',False)
110 if e:
111 res=self.pool.get("product.product").read(cr, uid, [vals['product_id']], ['name','ean_type'])[0]
112 unit_ean= (str(res['ean_type'])=='unit')
113 if unit_ean: vals['encoding']=False
114 else:
115 e= e[:7]
116 if not vals.get('encoding',False):
117 # this code has been changed to avoid SQL, but depends on lp:~frederic-declercq/openobject-server/server-fu-reference_browse
118 encoding = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ean_encoding
119 vals['encoding']= '%s,%s' % (encoding._table_name, encoding.id)
120 e+=('0'*(12-len(e)))
121 vals['ean']= self.ean_key(cr, uid, e)
122 return super(product_packaging, self).create(cr, uid, vals, context=context)
123
124 # reaches barcode through unit and weight EANs
125 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
126 rargs=[]
127 for arg in args:
128 if arg[0]=='ean':
129 # SQL required because 'startswith' isn't a valid operator
130 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]))
131 if len(args)==1: return [r[0] for r in cr.fetchall()]
132 rargs.append( ('id','in',[r[0] for r in cr.fetchall()]) )
133 else: rargs.append(arg)
134 return super(product_packaging, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
135
136 # function returning product id, name, ean type (price, weight, unit), price, quantity, amount from barcode
137 def ean_data(self, cr, uid, ean, pricelist_id, tax_included=True, partner_id=None, address_id=None, context={}):
138 ret= []
139 ids= self.search(cr, uid, [('ean','=',ean)])
140 if not ids: return ret
141 data_part= int(ean[7:12])
142 for b in self.browse(cr, uid, ids, context=context):
143 if b.encoding:
144 #obj,oid= b.encoding.split(',')
145 #e=self.pool.get(obj).browse(cr,uid,int(str(oid)))
146 r=[]
147 if b.encoding.rate:
148 # no use to apply lot prices on products with weight: scales can't use OpenERP pricelists
149 r= {
150 'id': b.product_id.id,
151 'name': b.product_id.name,
152 'type': 'price',
153 'uom_id': b.product_id.uom_id.id,
154 'amount': round(0.01*data_part/b.encoding.rate, 2),
155 'quantity': round( (0.01*data_part/b.encoding.rate) /b.product_id.list_price_tax_incl, 3 ) }
156 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]
157 else:
158 r= {
159 'id': b.product_id.id,
160 'name': b.product_id.name,
161 'type': 'weight',
162 'uom_id': b.product_id.uom_id.id,
163 'quantity': round(1.0*data_part/b.encoding.factor, b.encoding.rounding) }
164 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]
165 r['amount']= r['price']*r['quantity']
166 else:
167 pptype = self.pool.get('product.pricelist').read(cr, uid, [pricelist_id], ['type'])[0]['type']
168 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)
169 pkg_qty= b.qty*b.ul_qty*b.rows
170 r= {
171 'id': b.product_id.id,
172 'name': b.product_id.name,
173 'type': 'unit',
174 'uom_id': b.product_id.uom_id.id,
175 'quantity': (b.product_id.uom_id.id==uom) \
176 and pkg_qty or \
177 self.pool.get('product.uom')._compute_qty(cr, uid, uom, pkg_qty, b.product_id.uom_id.id),
178 'price': self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], b.product_id.id, 1.0, partner_id, context=context)[pricelist_id] }
179 r['amount']= r['price']
180 if tax_included:
181 taxes_id = self.pool.get('product.product').read(cr, uid, [r['id']],['taxes_id'])[0]['taxes_id']
182 taxes= self.pool.get( 'account.tax' ).browse(cr, uid, taxes_id, context=context)
183 taxes= self.pool.get( 'account.tax' ).compute( cr, uid, taxes, r['price'], r['quantity'], address_id, r['id'], partner_id )
184 r['price'] += sum([t['amount'] for t in taxes])
185 if r['type']!='price':
186 # on amount written into barcode, we have to respect amount
187 r['amount']= r['price']*r['quantity']
188 ret.append(r)
189 return ret
190
191product_packaging()
192
193class product_product( osv.osv ):
194 _name= "product.product"
195 _inherit= "product.product"
196
197 # check if unit or weight depending on uos / uom rounding
198 def _ean_type( self, cr, uid, ids, name, args, context={} ):
199 ret = {}
200 if not ids: return ret
201 for prod in self.browse(cr, uid, ids, context=context):
202 uom = prod.uos_id or prod.uom_id
203 ret[prod.id] = (uom.rounding == int(uom.rounding)) and 'unit' or 'price'
204 return ret
205
206 # old ean13 field becomes a function returning the first ean
207 def _ean13( self, cr, uid, ids, name, args, context={} ):
208 ret = {}
209 for b in self.browse(cr, uid, ids):
210 ret[b.id]=(b.packaging) and b.packaging[0].ean or False
211 return ret
212
213 _columns = {
214 "ean13": fields.function(_ean13, method=True, type='char', size=20, string='EAN13', readonly=True, store=False),
215 "ean_type": fields.function(_ean_type, method=True, type='selection', selection=[('unit','Unit'),('price','Price')], string='EAN type', readonly=True),
216 }
217
218 # reaches any barcode from name field
219 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
220 rargs=[]
221 for arg in args:
222 if (arg[0]=='name') and not re.sub('[0-9]','',arg[2]):
223 # SQL required because 'startswith' isn't recognized as operator
224 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]))
225 if len(args)==1: return [r[0] for r in cr.fetchall()]
226 rargs.append( ('id','in',[r[0] for r in cr.fetchall()]) )
227 else: rargs.append(arg)
228 return super(product_product, self).search(cr, uid, rargs, offset=offset, limit=limit, order=order, context=context, count=count)
229
230 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=80):
231 if not args:
232 args=[]
233 if not context:
234 context={}
235 if name:
236 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
237 if not len(ids):
238 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
239 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
240 else:
241 ids = self.search(cr, user, args, limit=limit, context=context)
242 result = self.name_get(cr, user, ids, context)
243 return result
244
245 # if no barcode set on create, it creates one (if sale_ok)
246 def create( self , cr, uid, vals, context={} ):
247 if vals.get('sale_ok',False) and not vals.get('packaging',[]):
248 uom_rounding = self.pool.get('product.uom').read(cr, uid, [vals.get('uos_id',False) or vals.get('uom_id',False)], ['rounding'])[0]['rounding']
249 ean_unit = uom_rounding == int(uom_rounding)
250 ean= self.pool.get('product.packaging').new_ean(cr,uid,[], ean_unit)
251 ul = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ul.id
252 vals['packaging']=[(0,0,{'rows':1, 'sequence': 1, 'ean': ean, 'ul': ul, 'qty': 1, 'ul_qty': 1})]
253 ret= super( product_product, self ).create(cr,uid,vals, context=context )
254 return ret
255
256 # on obsolete, drops all eans for this product
257 # on uos / uom change if ean type changed, checks eans and corrects if required
258 def write( self , cr, uid, ids, vals, context={} ):
259 if vals.get('state','draft')=='obsolete':
260 ret= super( product_product, self ).write( cr, uid, ids, vals, context=context )
261 pp_ids = self.pool.get('product.packaging').search(cr, uid, [('product_id','in',ids)], context=context)
262 self.pool.get('product.packaging').unlink(cr, uid, ppids, context=context)
263 return ret
264 types= {}
265 for b in self.browse(cr, uid, ids):
266 types[b.id]= b.ean_type
267 ean_alert= False
268 for n in vals.get('packaging',[]):
269 if n[0] and n[2].get('ean',False): ean_alert=True
270 if ean_alert:
271 if 'packaging' in vals:
272 super( product_product, self ).write( cr,uid,ids,{'packaging':vals['packaging']}, context=context )
273 del vals['packaging']
274 for r in self.browse(cr, uid, ids,context=context):
275 if not r.packaging:
276 uom = r.uos_id or r.uom_id
277 uom_rounding = uom.rounding==int(uom.rounding)
278 ean= self.pool.get('product.packaging').new_ean(cr,uid,[], uom_rounding)
279 ul = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.ul.id
280 cr.execute("""SELECT seq
281 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
282 LEFT JOIN product_packaging p ON p.sequence=s.seq AND p.product_id=s.product_id
283 WHERE p.sequence IS NULL
284 ORDER BY 1 DESC LIMIT 1""" % (r.id,r.id))
285 seq= cr.fetchone()[0]
286 v = {'packaging': [(0,0,{'rows':1, 'sequence': seq, 'ean': ean, 'ul': ul, 'qty': 1, 'ul_qty': 1})] }
287 super( product_product, self ).write( cr,uid,r.id, v, context=context )
288 ret= super( product_product, self ).write( cr,uid,ids,vals, context=context )
289 return ret
290
291 # on delete, drops all eans for this product
292 # for security, sets ean13 to null too
293 def unlink( self, cr, uid, ids, context={} ):
294 pp_ids = self.pool.get('product.packaging').search(cr, uid, [('product_id', 'in', ids)])
295 self.pool.get('product.packaging').unlink(cr, uid, pp_ids, context=context)
296 return super( product_product, self ).unlink(cr, uid, ids, context=context)
297
298product_product()
0299
=== added file 'product_multibarcode/product_view.xml'
--- product_multibarcode/product_view.xml 1970-01-01 00:00:00 +0000
+++ product_multibarcode/product_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,42 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data>
4
5 <record id="product_normal_form_view" model="ir.ui.view">
6 <field name="name">product.normal.form</field>
7 <field name="model">product.product</field>
8 <field name="inherit_id" ref="product.product_normal_form_view"/>
9 <field name="type">form</field>
10 <field name="arch" type="xml">
11 <field name="packaging" position="replace">
12 <label string="" colspan="2"/>
13 <field name="ean_type" colspan="2" readonly="1"/>
14 <field colspan="4" name="packaging" nolabel="1">
15 <form string="Packaging">
16 <field name="ean" select="1"/>
17 <field name="ttf"/>
18 <newline/>
19 <field name="sequence"/>
20 <field name="encoding" on_change="onchange_encoding(encoding)"/>
21 <newline/>
22 <field name="qty" select="1"/>
23 <field name="ul"/>
24 <field name="weight_ul"/>
25 <separator colspan="4" string="Palletization"/>
26 <field name="ul_qty"/>
27 <field name="rows"/>
28 <field name="weight"/>
29 <separator colspan="4" string="Pallet Dimension"/>
30 <field name="height"/>
31 <field name="width"/>
32 <field name="length"/>
33 <separator colspan="4" string="Other Info"/>
34 <field colspan="4" name="name" select="1"/>
35 </form>
36 </field>
37 </field>
38 </field>
39 </record>
40
41 </data>
42</openerp>
043
=== added file 'product_multibarcode/res.py'
--- product_multibarcode/res.py 1970-01-01 00:00:00 +0000
+++ product_multibarcode/res.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,15 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4
5class res_company( osv.osv ):
6 _name= "res.company"
7 _inherit= "res.company"
8 _columns= {
9 # default object to base method for encoding weighted barcodes
10 # if currency: 0.01 * rate * [ EAN data ]
11 # if unit: factor * [ EAN data ]
12 "ean_encoding": fields.reference('EAN weight', selection=[('res.currency','Currency'),('product.uom','Unit')], size=128, required=True, help="Default base for EAN13 weight encoding"),
13 '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"),
14 }
15res_company()
016
=== added file 'product_multibarcode/res_view.xml'
--- product_multibarcode/res_view.xml 1970-01-01 00:00:00 +0000
+++ product_multibarcode/res_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,21 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data>
4
5 <record id="view_company_form" model="ir.ui.view">
6 <field name="name">res.company.form</field>
7 <field name="model">res.company</field>
8 <field name="inherit_id" ref="base.view_company_form"/>
9 <field name="type">form</field>
10 <field name="arch" type="xml">
11 <page string="Configuration" position="inside">
12 <separator colspan="4" string="Barcodes"/>
13 <field name="ean_encoding"/>
14 <field name="ul"/>
15 </page>
16 </field>
17 </record>
18
19 </data>
20</openerp>
21
022
=== added directory 'product_structure'
=== added file 'product_structure/__init__.py'
--- product_structure/__init__.py 1970-01-01 00:00:00 +0000
+++ product_structure/__init__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,5 @@
1# -*- coding: utf-8 -*-
2
3import product
4import purchase
5import res
0\ No newline at end of file6\ No newline at end of file
17
=== added file 'product_structure/__terp__.py'
--- product_structure/__terp__.py 1970-01-01 00:00:00 +0000
+++ product_structure/__terp__.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,42 @@
1# -*- coding: utf-8 -*-
2
3{
4 "name" : "Product structure",
5 "description" :
6 """
7Adds typed multi levels human readable structure on product categories
8
9Defaults values for products depending on categories & children:
10- tax
11- payment term
12- email
13- product manager
14
15Adds also:
16- product categories on partners
17- product category on purchase orders with search on category & supplier for products
18- products list on partner from supplierinfo
19- partners on supplierinfo found as suppliers even if not set
20__________________________________________
21=> descriptions & screenshots:
22 http://www.lafermedusart.com/modules/product_structure.html
23
24All modules from Fermes Urbaines are put on launchpad when used on production.
25We just offer support to Fermes Urbaines partners.
26Eventhought, all addons are updated as soon as tested.
27""",
28 "version" : "1.0.0",
29 "depends" : [ "purchase" ],
30 "author" : "Fermes Urbaines",
31 "website" : "http://www.lafermedusart.com/modules/product_structure.html",
32 "category" : "Generic Modules/Base",
33 "init_xml" : [ ],
34 "demo_xml" : [ ],
35 "update_xml" : [
36 "product_view.xml",
37 "purchase_view.xml",
38 "res_view.xml"
39 ],
40 "active": True,
41 "installable" : True
42}
043
=== added file 'product_structure/alt_osv.py'
--- product_structure/alt_osv.py 1970-01-01 00:00:00 +0000
+++ product_structure/alt_osv.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,51 @@
1# -*- coding: utf-8 -*-
2
3from osv import osv
4
5class alt_osv(osv.osv):
6 """Proposal for an extension on osv.osv:
7
8All fields in _recursive list have recursion value. To avoid recursion (ie to configure), you can set {'recursion': False} in context.
9Recursion can be done if special field 'parent_id' is on recursion model."""
10
11 _recursive = []
12
13 def read(self, cr, uid, ids, field_names=None, context=None, load='_classic_read'):
14 """Browse() depends on read(). Changing read() changes browse()."""
15
16 columns = self._columns.keys() + self._inherit_fields.keys()
17 if (not 'parent_id' in columns) or (not self._recursive):
18 return super(alt_osv, self).read(cr, uid, ids, fields=field_names, context=context, load=load)
19
20 if not context: context = {}
21 delparent = False
22 if field_names and not 'parent_id' in field_names:
23 field_names.append( 'parent_id' )
24 delparent = True
25 data = super(alt_osv, self).read(cr, uid, ids, fields=field_names, context=context, load=load)
26 if (not data) or not context.get('recursion', True):
27 if delparent:
28 for d in data: del d['parent_id']
29 return data
30
31 if not field_names:
32 field_names = [ f for f in data[0] if f!='id' ]
33 recursive = [ f for f in self._recursive if f in field_names ]
34 explore = {}
35 for d in data:
36 if d['parent_id']:
37 parent_id = (type(d['parent_id']) in (int,long)) and d['parent_id'] or d['parent_id'][0]
38 explore[d['id']] = (parent_id, [ f for f in recursive if not d[f] ])
39 if not explore[d['id']][1]: del explore[d['id']]
40 if explore:
41 exp = {}
42 for data_id in explore:
43 exp[data_id] = self.read(cr, uid, [explore[data_id][0]], explore[data_id][1], context=context, load=load)[0]
44 del exp[data_id]['id']
45 if 'parent_id' in exp[data_id]: del exp[data_id]['parent_id']
46 for d in data:
47 if d['id'] in exp:
48 print exp[ d['id'] ]
49 d.update( exp[ d['id'] ] )
50 if delparent: del d['parent_id']
51 return data
052
=== added file 'product_structure/product.py'
--- product_structure/product.py 1970-01-01 00:00:00 +0000
+++ product_structure/product.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,366 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4import alt_osv
5from tools.translate import _
6
7
8
9PRODUCT_TYPES = [
10 ('product', _('Stockable Product')),
11 ('consu', _('Consumable')),
12 ('service', _('Service')),
13 ('fee', _('Fee')),
14 ('recurrent', _('Recurrent')),
15 ('recordable', _('Recordable Product')),
16 ('hr', _('HR')) ]
17
18def categ_manager(categ):
19 """Manager for a category (recursive function)
20IN: browse on product.category
21OUT: res.users id"""
22 return (categ.user_id) and categ.user_id.id or (categ.parent_id) and categ_manager(categ.parent_id) or False
23
24def manager_categories(categ):
25 """Categories for a manager
26IN: browse on product.category
27OUT: product.category list of ids"""
28 category_ids= [ categ.id ]
29 if categ.child_id:
30 for c in categ.child_id:
31 category_ids.extend( manager_categories(c) )
32 return category_ids
33
34def type_categories(categ):
35 """Children categories until type change
36IN: browse on product.category
37OUT: product.category list of ids"""
38 category_ids= [ categ.id ]
39 for c in categ.child_id:
40 if not c.type_id:
41 category_ids.extend( type_categories(c) )
42 return category_ids
43
44
45
46class product_category_type(osv.osv):
47 """Hierarchy levels on categories.
48Assign 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"""
49 _name = "product.category.type"
50 _description = "Product Category Type"
51
52 def _child_id( self, cr, uid, ids, name, args, context={} ):
53 ret = {}
54 for i in ids:
55 ct= self.search( cr, uid, [('parent_id','=',i)] )
56 ret[i]= (ct) and ct[0] or i
57 return ret
58
59 _columns= {
60 "name": fields.char("Name", required=True, size=64),
61 "allow_products": fields.boolean("Product category", help="If not checked, all categories of this level won't be proposed to create products"),
62 "parent_id": fields.many2one('product.category.type','Parent'),
63 'child_id': fields.function(_child_id, method=True, type='many2one', relation='product.category.type', obj='product.category.type', string='Child Type', readonly=True),
64 }
65 _defaults= {
66 "allow_products": lambda *a: False,
67 }
68 _sql_constraints= [ ('unique_parent_id','unique (parent_id)','This parent is already used') ]
69
70product_category_type()
71
72
73class product_category(alt_osv.alt_osv):
74 _name = "product.category"
75 _inherit = "product.category"
76
77 def _type( self, cr, uid, ids, name, args, context={} ):
78 ret = {}
79 for b in self.browse(cr, uid, ids, context={'recursion':False}):
80 c= b
81 n=0
82 while c.parent_id and not c.type_id:
83 c=c.parent_id
84 n+=1
85 if c.type_id:
86 d=c.type_id
87 while n>0:
88 d=d.child_id
89 n=n-1
90 ret[b.id]= d.id
91 else: ret[b.id]= False
92 return ret
93
94 def _allow_products( self, cr, uid, ids, name, args, context={} ):
95 ret = {}
96 for b in self.browse(cr, uid, ids, context={'recursion':False}):
97 ret[b.id]= (b.relate_type_id) and b.relate_type_id.allow_products or False
98 return ret
99
100 def _parent_node( self, cr, uid, ids, name, args, context={} ):
101 ret = {}
102 for b in self.browse(cr, uid, ids, context={'recursion':False}):
103 c = b
104 while c.parent_id and not c.type_id: c=c.parent_id
105 ret[b.id] = c.id
106 return ret
107
108 def _partner_ids( self, cr, uid, ids, name, args, context={} ):
109 ret = {}
110 product_children = self.get_product_children(cr, uid, ids, context=context)
111 for cid in product_children:
112 if not product_children[cid]:
113 ret[cid] = []
114 continue
115 cr.execute("""SELECT s.name
116 FROM product_supplierinfo s, product_template t, product_product p
117 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)
118 GROUP BY 1""" % ','.join(map(str, product_children[cid])))
119 ret[cid]= (cr.rowcount) and [ r[0] for r in cr.fetchall() ] or []
120 return ret
121
122 def get_product_children(self, cr, uid, ids, context = None):
123 """Returns children categories in the same hierarchy that allows product.
124Add {'browse': True} in context, if you want browse objects instead of a list of ids"""
125 product_children = {}
126 res_browse = (context or {}).get('browse', False)
127 ctx = (context or {}).copy()
128 ctx.update({'recursion':False})
129 for b in self.browse(cr, uid, ids, context=ctx):
130 prods = []
131 a = [ b ]
132 while a:
133 r = []
134 for c in a:
135 if c.type_id and c.id!=b.id: break
136 if c.allow_products: prods.append((res_browse) and c or c.id)
137 r.extend( c.child_id )
138 a = r+[]
139 product_children[b.id] = prods
140 return product_children
141
142 _recursive = ['email','type_id','qty_alert_min','qty_alert_max','user_id','auto_picking','term_id','taxes_id','supplier_taxes_id','product_type']
143
144 _columns = {
145 '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'"),
146 "email": fields.char('Email', size=128, help="Public email to contact someone responsible"),
147 "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)"),
148 'qty_alert_min': fields.integer('Qty alert (min)'),
149 'qty_alert_max': fields.integer('Qty alert (max)'),
150 "user_id": fields.many2one('res.users','Manager'),
151 "auto_picking": fields.boolean("No remainder"),
152 "parent_node_id": fields.function(_parent_node, method=True, type='many2one', relation='product.category', obj='product.category', string='Type node', readonly=True, select=1),
153 "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),
154 "allow_products": fields.function(_allow_products, method=True, type='boolean', string="Product category", readonly=True, select=1),
155 "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'),
156 "term_id": fields.many2one('account.payment.term','Payment term'),
157 'taxes_id': fields.many2many('account.tax', 'product_taxes_rel',
158 'prod_id', 'tax_id', 'Customer Taxes',
159 domain=[('parent_id','=',False),('type_tax_use','in',['sale','all'])], help="Taxes by default on products from this category"),
160 'supplier_taxes_id': fields.many2many('account.tax',
161 'product_supplier_taxes_rel', 'prod_id', 'tax_id',
162 'Supplier Taxes', domain=[('parent_id', '=', False),('type_tax_use','in',['purchase','all'])], help="Supplier taxes by default on products from this category"),
163 'product_type': fields.selection(PRODUCT_TYPES, 'Product Type',
164 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."),
165 }
166
167 def name_get(self, cr, uid, ids, context=None):
168 """Return hierarchy path from the first type node"""
169 ret = []
170 for b in self.browse(cr, uid, ids, context=context):
171 txts= []
172 a=b
173 while a.parent_id and not a.type_id:
174 a= a.parent_id
175 txts.append( a.name )
176 txts.reverse()
177 txt= ('»').decode('utf-8').join(txts)
178 txt= (txt) and b.name+' ('+txt+')' or b.name
179 ret.append( (b.id, txt) )
180 return ret
181
182 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
183 """Allows searching on function fields: user_id, relate_type_id, allow_products
184Allows expressions on searching category in hierarchy as PART-OF-TYPE:PART-OF-NAME+PART-OF-PARENT-NAME+..."""
185 rgs= []
186 for arg in args:
187 tids= []
188
189 if arg[0]=='user_id':
190 ids= self.search(cr, uid, [('user_id',arg[1],arg[2])], offset=offset, limit=limit, order=order, context=context, count=count)
191 if ids:
192 categ_ids= []
193 for b in self.browse(cr, uid, ids):
194 categ_ids.extend( manager_categories(b) )
195 rgs.append( ('id','in', categ_ids ) )
196 else: return []
197
198 elif arg[0]=='relate_type_id':
199 if type(arg[2]) in (int, long): tids= [arg[2]]
200 elif arg[1]=='in': tids= arg[2][0]
201 elif type(arg[2]) in (tuple, list): tids= [arg[2][0]]
202 else:
203 tids= self.pool.get('product.category.type').search(cr, uid, [('name',arg[1],arg[2])], context=context)
204
205 elif arg[0]=='allow_products':
206 tids= self.pool.get('product.category.type').search(cr, uid, [ arg ], context=context)
207
208 elif arg[0]=='name':
209 nam= arg[2]
210 v= [a.strip() for a in arg[2].split(':')][:2]
211 if len(v)==2:
212 typ, nam= v
213 args.append( ('relate_type_id', arg[1], typ) )
214 criterias= [a.strip() for a in nam.split('+')]
215 criteria= criterias.pop(0)
216 if criteria: rgs.append( ('name', arg[1], criteria) )
217 if criterias:
218 criteria= criterias.pop(0)
219 if criteria: rgs.append( ('parent_id', arg[1], criteria) )
220 n=1
221 while criterias:
222 criteria= criterias.pop(0)
223 if criteria:
224 ids= super(product_category, self).search(cr, uid, [ ('parent_id', arg[1], criteria) ], context=context)
225 cids= []
226 m=0
227 while m<n:
228 if not cids: cids= ids
229 cids= super(product_category, self).search(cr, uid, [ ('parent_id', 'in', cids) ], context=context)
230 m = (cids) and m+1 or n
231 if not cids: return cids
232 rgs.append( ('id', 'in', cids) )
233 n+=1
234
235 else: rgs.append(arg)
236
237 if tids:
238 reftypes= {}
239 for t in self.pool.get('product.category.type').browse(cr, uid, tids, context=context):
240 a= t
241 n= 0
242 while a.parent_id:
243 a = a.parent_id
244 n+=1
245 if not a.id in reftypes: reftypes[a.id]= {}
246 reftypes[a.id][t.id]= (n, t.child_id.id==t.id)
247 cids= super(product_category, self).search(cr, uid, [('type_id','in',reftypes.keys())], context=context)
248 categ_ids= []
249 for b in self.browse(cr, uid, cids, context=context):
250 for tid in reftypes[b.type_id.id]:
251 n= 0
252 c=[b]
253 while n<reftypes[b.type_id.id][tid][0]:
254 t= []
255 for a in c: t.extend(a.child_id)
256 c= t
257 n+=1
258 if reftypes[b.type_id.id][tid][1]:
259 for t in c: categ_ids.extend( type_categories(t) )
260 else: categ_ids.extend( [t.id for t in c ] )
261 rgs.append( ('id','in', categ_ids ) )
262
263 return super(product_category, self).search(cr, uid, rgs, offset=offset, limit=limit, order=order, context=context, count=count)
264
265product_category()
266
267
268
269class product_template( osv.osv ):
270 _name = "product.template"
271 _inherit = "product.template"
272
273 def _product_manager( self, cr, uid, ids, name, args, context={} ):
274 ret = {}
275 for b in self.browse(cr, uid, ids):
276 ret[b.id]=b.categ_id.user_id.id
277 return ret
278
279 # On change categ_id, inherits default values from category
280 # Right side argument, is category field name if different from product field name
281 _inherit_defaults = {
282 'categ_id': [
283 ('product_manager', 'user_id'),
284 ('qty_alert_min',),
285 ('qty_alert_max',),
286 ('taxes_id',),
287 ('supplier_taxes_id',),
288 ('type', 'product_type') ],
289 }
290
291 _columns= {
292 "product_manager": fields.function(_product_manager, method=True, type='many2one', relation='res.users', string='Categ manager', select=1, readonly=True),
293 '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'"),
294 'qty_alert_min': fields.float('Qty alert (min)', digits=(16, 3)),
295 'qty_alert_max': fields.float('Qty alert (max)', digits=(16, 3)),
296 'type': fields.selection(PRODUCT_TYPES, 'Product Type', required=True,
297 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."),
298 }
299
300 def onchange_categ_id(self, cr, uid, ids, ref_id, context=None):
301 ret = {'value': {} }
302# for f in self._inherit_defaults:
303# if isinstance(self._columns[f[0]], fields.one2many) or isinstance(self._columns[f[0]], fields.many2many):
304# ret['value'][f[0]] = []
305 if not ref_id: return ret
306 obj = self._columns['categ_id']._obj
307 res = self.pool.get(obj).browse(cr, uid, ref_id, context=context)
308 for f in self._inherit_defaults['categ_id']:
309 val = eval("res."+((len(f)==1) and f[0] or f[1]))
310 if isinstance(val, osv.orm.browse_null):
311 ret['value'][f[0]] = False
312 continue
313 if isinstance(self._columns[f[0]], fields.one2many) or isinstance(self._columns[f[0]], fields.many2many):
314 ret['value'][f[0]] = [v.id for v in val]
315 elif isinstance(self._columns[f[0]], fields.many2one):
316 ret['value'][f[0]] = val.id
317 else:
318 ret['value'][f[0]] = val
319 return ret
320
321product_template()
322
323
324
325def compare_suppliers(a,b):
326 if not a[2]: return 1
327 if not b[2]: return -1
328 if not a[2]['action']=='none': return 1
329 if not a[2]['action']=='create': return -1
330 return (b[2]['action']=='create') and 1 or -1
331
332class product_product( osv.osv ):
333 _name = "product.product"
334 _inherit = "product.product"
335
336 def onchange_categ_id(self, cr, uid, ids, categ_id):
337 if not ids: return self.pool.get('product.template').onchange_categ_id(cr, uid, [], categ_id)
338 pids = [prod['product_tmpl_id'][0] for prod in self.read(cr, uid, ids, ['product_tmpl_id'])]
339 return self.pool.get('product.template').onchange_categ_id(cr, uid, pids, categ_id)
340
341 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
342 rargs=[]
343 for arg in args:
344 cids=[]
345 if arg[0]=='categ_id':
346 if type(arg[2]) in (int, long): cids= [arg[2]]
347 elif arg[1]=='in': cids= arg[2]
348 elif type(arg[2]) in (list, tuple): cids= [arg[2][0]]
349 else:
350 cids= self.pool.get("product.category").search(cr,uid,[('name',arg[1],arg[2])])
351
352 # LIGNES À RETIRER (conservées par acquis de conscience):
353 #for b in self.pool.get("product.category").browse(cr, uid, cat_ids, context=context):
354 #cids.extend( manager_categories(b) )
355
356 if arg[0]=='product_manager':
357 cids= self.pool.get("product.category").search(cr,uid,[('user_id',arg[1], arg[2])])
358
359 if cids:
360 rargs.append( ('categ_id','in',cids) )
361 elif arg[0] in ('categ_id', 'product_manager'): return []
362
363 if not (arg[0] in ('categ_id','product_manager') or (arg[0]=='seller_ids' and type(arg[2]) in (int,long))): rargs.append(arg)
364 return super(product_product, self).search(cr, uid,rargs, offset=offset, limit=limit, order=order, context=context, count=count)
365
366product_product()
0367
=== added file 'product_structure/product_view.xml'
--- product_structure/product_view.xml 1970-01-01 00:00:00 +0000
+++ product_structure/product_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,145 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data>
4
5 <record id="product_category_type_form_view" model="ir.ui.view">
6 <field name="name">product.category.type.form</field>
7 <field name="model">product.category.type</field>
8 <field name="type">form</field>
9 <field name="arch" type="xml">
10 <form string="Category type">
11 <field name="name"/>
12 <field name="allow_products"/>
13 <field name="parent_id"/>
14 <field name="child_id"/>
15 </form>
16 </field>
17 </record>
18
19 <record id="product_category_type_tree_view" model="ir.ui.view">
20 <field name="name">product.category.type.tree</field>
21 <field name="model">product.category.type</field>
22 <field name="type">tree</field>
23 <field name="arch" type="xml">
24 <tree string="Category type">
25 <field name="parent_id"/>
26 <field name="name"/>
27 <field name="child_id"/>
28 </tree>
29 </field>
30 </record>
31
32 <record id="product_category_type_action" model="ir.actions.act_window">
33 <field name="name">Category types</field>
34 <field name="type">ir.actions.act_window</field>
35 <field name="res_model">product.category.type</field>
36 <field name="view_type">form</field>
37 <field name="view_id" ref="product_category_type_tree_view"/>
38 </record>
39
40 <menuitem action="product_category_type_action" id="menu_product_category_type" parent="product.menu_config_product" groups="product.group_product_manager"/>
41
42 <record id="product_category_list_view2" model="ir.ui.view">
43 <field name="name">product.category.list</field>
44 <field name="model">product.category</field>
45 <field name="type">tree</field>
46 <field name="priority">0</field>
47 <field name="arch" type="xml">
48 <tree string="Product Categories">
49 <field name="name"/>
50 <field name="parent_node_id"/>
51 <field name="allow_products"/>
52 <field name="parent_id"/>
53 </tree>
54 </field>
55 </record>
56
57 <record id="product.product_category_action_form" model="ir.actions.act_window">
58 <field name="name">Products Categories</field>
59 <field name="type">ir.actions.act_window</field>
60 <field name="res_model">product.category</field>
61 <field name="view_type">form</field>
62 <field name="view_id" ref="product_category_list_view2"/>
63 </record>
64
65 <record id="product_category_form_view1" model="ir.ui.view">
66 <field name="name">product.category.form</field>
67 <field name="model">product.category</field>
68 <field name="inherit_id" ref="product.product_category_form_view"/>
69 <field name="type">form</field>
70 <field name="arch" type="xml">
71 <field name="sequence" position="after">
72 <field name="parent_node_id"/>
73 <separator colspan="4" string="Products configuration"/>
74 <group colspan="2" col="3">
75 <field name="type_id"/>
76 <field name="user_id"/>
77 <field name="email"/>
78 <field name="term_id"/>
79 </group>
80 <group colspan="2" col="3">
81 <field name="parent_node_id" colspan="3"/>
82 <field name="qty_alert_min" string="Qty alert (min/max)"/>
83 <field name="qty_alert_max" nolabel="1"/>
84 <field name="allow_products"/>
85 <label string=" " colspan="1"/>
86 </group>
87 <separator colspan="2" string="Supply taxes (default for products)"/>
88 <separator colspan="2" string="Sale taxes (default for products)"/>
89 <field name="supplier_taxes_id" nolabel="1" colspan="2"/>
90 <field name="taxes_id" nolabel="1" colspan="2"/>
91 <field name="partner_ids" colspan="4" nolabel="1"/>
92 </field>
93 </field>
94 </record>
95
96 <record id="product.product_category_action_form" model="ir.actions.act_window">
97 <field name="name">Products Categories</field>
98 <field name="type">ir.actions.act_window</field>
99 <field name="res_model">product.category</field>
100 <field name="context">{'recursion':False}</field>
101 <field name="view_type">form</field>
102 <field name="view_id" ref="product.product_category_list_view"/>
103 </record>
104
105 <record id="product_normal_form_view1" model="ir.ui.view">
106 <field name="name">product.normal.form</field>
107 <field name="model">product.product</field>
108 <field name="inherit_id" ref="product.product_normal_form_view"/>
109 <field name="type">form</field>
110 <field name="arch" type="xml">
111 <field name="categ_id" position="replace">
112 <field name="categ_id" on_change="onchange_categ_id(categ_id)"/>
113 </field>
114 </field>
115 </record>
116
117 <record id="product_normal_form_view2" model="ir.ui.view">
118 <field name="name">product.normal.form</field>
119 <field name="model">product.product</field>
120 <field name="inherit_id" ref="product.product_normal_form_view"/>
121 <field name="type">form</field>
122 <field name="arch" type="xml">
123 <field name="product_manager" position="replace">
124 <group colspan="2" col="3">
125 <field name="qty_alert_min" string="Qty alert (min/max)"/>
126 <field name="qty_alert_max" nolabel="1"/>
127 </group>
128 </field>
129 </field>
130 </record>
131
132 <record id="product_normal_form_view3" model="ir.ui.view">
133 <field name="name">product.normal.form</field>
134 <field name="model">product.product</field>
135 <field name="inherit_id" ref="stock.view_normal_stock_property_form"/>
136 <field name="type">form</field>
137 <field name="arch" type="xml">
138 <field name="virtual_available" position="after">
139 <field name="product_manager"/>
140 </field>
141 </field>
142 </record>
143
144 </data>
145</openerp>
0146
=== added file 'product_structure/purchase.py'
--- product_structure/purchase.py 1970-01-01 00:00:00 +0000
+++ product_structure/purchase.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,27 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4
5
6
7class purchase_order(osv.osv):
8 _name = 'purchase.order'
9 _inherit = 'purchase.order'
10
11 _columns= {
12 'categ_id': fields.many2one('product.category','Category', change_default=True),
13 }
14
15 def onchange_partner_id(self, cr, uid, ids, partner_id):
16 ret = super( purchase_order, self ).onchange_partner_id(cr, uid, ids, partner_id)
17 if not partner_id: return ret
18
19 categs = self.pool.get("res.partner").read(cr, uid, [ partner_id ], ['categ_ids'])
20 if not categs: return ret
21
22 ret['domain'] = ret.get('domain', {})
23 ret['domain']['categ_id']= [('id', 'in', categs[0]['categ_ids'])]
24 if len(categs[0]['categ_ids']) == 1: ret['value']['categ_id'] = categs[0]['categ_ids'][0]
25 return ret
26
27purchase_order()
028
=== added file 'product_structure/purchase_view.xml'
--- product_structure/purchase_view.xml 1970-01-01 00:00:00 +0000
+++ product_structure/purchase_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,30 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data>
4
5 <record id="purchase_order_form" model="ir.ui.view">
6 <field name="name">purchase.order.form</field>
7 <field name="model">purchase.order</field>
8 <field name="type">form</field>
9 <field name="inherit_id" ref="purchase.purchase_order_form"/>
10 <field name="arch" type="xml">
11 <field name="shipped" position="after">
12 <field name="categ_id" on_change="categ_id_change(categ_id,context)"/>
13 </field>
14 </field>
15 </record>
16
17 <record id="purchase_order_line_form" model="ir.ui.view">
18 <field name="name">purchase.order.line.form</field>
19 <field name="model">purchase.order.line</field>
20 <field name="type">form</field>
21 <field name="inherit_id" ref="purchase.purchase_order_line_form"/>
22 <field name="arch" type="xml">
23 <field name="product_id" position="replace">
24 <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)]"/>
25 </field>
26 </field>
27 </record>
28
29 </data>
30</openerp>
031
=== added file 'product_structure/res.py'
--- product_structure/res.py 1970-01-01 00:00:00 +0000
+++ product_structure/res.py 2009-12-16 13:50:35 +0000
@@ -0,0 +1,47 @@
1# -*- coding: utf-8 -*-
2
3from osv import fields, osv
4
5
6
7class res_partner(osv.osv):
8 _name = "res.partner"
9 _inherit = "res.partner"
10
11 def _categ_ids( self, cr, uid, ids, name, args, context={} ):
12 ret = {}
13 for i in ids: ret[i] = []
14 cids = self.pool.get("product.category").search(cr, uid, [("type_id","=","")], context={'recursion':False})
15 for r in self.pool.get("product.category").read(cr, uid, cids, ["partner_ids"]):
16 for p in r['partner_ids']:
17 if p in ret: ret[p].append( r['id'] )
18 return ret
19
20 _columns = {
21 "categ_ids": fields.function(_categ_ids, method=True, type='one2many', relation='product.category', string='Categorie', select=1, readonly=True),
22 "product_ids": fields.many2many('product.product', 'product_supplierinfo', 'name', 'product_id', 'Products', readonly=True),
23 }
24
25res_partner()
26
27
28
29class res_users( osv.osv ):
30 _name = 'res.users'
31 _inherit = 'res.users'
32
33 _columns = {
34 'context_categ_id': fields.many2one('product.category','Category'),
35 }
36
37 def context_get(self, cr, uid, context=None):
38 user = self.browse(cr, uid, uid, context)
39 result = super(res_users, self).context_get(cr, uid, context=context)
40 for k in self._columns.keys():
41 if k.startswith('context_'):
42 result[k[8:]] = ( isinstance(getattr(user, k), osv.orm.browse_record) and not isinstance(getattr(user, k), osv.orm.browse_null) ) \
43 and getattr(user,k).id \
44 or getattr(user,k)
45 return result
46
47res_users()
048
=== added file 'product_structure/res_view.xml'
--- product_structure/res_view.xml 1970-01-01 00:00:00 +0000
+++ product_structure/res_view.xml 2009-12-16 13:50:35 +0000
@@ -0,0 +1,35 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data>
4
5 <record id="view_partner_form" model="ir.ui.view">
6 <field name="name">res.partner.form</field>
7 <field name="model">res.partner</field>
8 <field name="type">form</field>
9 <field name="inherit_id" ref="base.view_partner_form"/>
10 <field name="arch" type="xml">
11 <notebook position="inside">
12 <page string="Products">
13 <separator string="Products" colspan="3"/>
14 <separator string="Products categories" colspan="1"/>
15 <field name="product_ids" nolabel="1" colspan="3"/>
16 <field name="categ_ids" nolabel="1" colspan="1"/>
17 </page>
18 </notebook>
19 </field>
20 </record>
21
22 <record id="view_users_form_simple_modif" model="ir.ui.view">
23 <field name="name">res.users.form.modif</field>
24 <field name="model">res.users</field>
25 <field name="type">form</field>
26 <field name="inherit_id" ref="base.view_users_form_simple_modif"/>
27 <field name="arch" type="xml">
28 <field name="signature" position="after">
29 <field name="context_categ_id"/>
30 </field>
31 </field>
32 </record>
33
34 </data>
35</openerp>

Subscribers

People subscribed via source and target branches