[FIX]product_visible_discount: add a company in product_id_change method
[odoo/odoo.git] / addons / document / document.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import base64
23 from osv import osv, fields
24 import os
25
26 # from psycopg2 import Binary
27 #from tools import config
28 import tools
29 from tools.translate import _
30 import nodes
31 import logging
32
33 DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['root_path'], 'filestore'))
34
35 class document_file(osv.osv):
36     _inherit = 'ir.attachment'
37     _rec_name = 'datas_fname'
38
39     def _attach_parent_id(self, cr, uid, ids=None, context=None):
40         """Migrate ir.attachments to the document module.
41
42         When the 'document' module is loaded on a db that has had plain attachments,
43         they will need to be attached to some parent folder, and be converted from
44         base64-in-bytea to raw-in-bytea format.
45         This function performs the internal migration, once and forever, for these
46         attachments. It cannot be done through the nominal ORM maintenance code,
47         because the root folder is only created after the document_data.xml file
48         is loaded.
49         It also establishes the parent_id NOT NULL constraint that ir.attachment
50         should have had (but would have failed if plain attachments contained null
51         values).
52         It also updates the  File Size for the previously created attachments.
53         """
54
55         parent_id = self.pool.get('document.directory')._get_root_directory(cr,uid)
56         if not parent_id:
57             logging.getLogger('document').warning("at _attach_parent_id(), still not able to set the parent!")
58             return False
59
60         if ids is not None:
61             raise NotImplementedError("Ids is just there by convention! Don't use it yet, please.")
62
63         cr.execute("UPDATE ir_attachment " \
64                     "SET parent_id = %s, db_datas = decode(encode(db_datas,'escape'), 'base64') " \
65                     "WHERE parent_id IS NULL", (parent_id,))
66
67         cr.execute("UPDATE ir_attachment SET file_size=length(db_datas) WHERE file_size = 0 and type = 'binary'")
68
69         cr.execute("ALTER TABLE ir_attachment ALTER parent_id SET NOT NULL")
70
71         return True
72
73     def _get_filestore(self, cr):
74         return os.path.join(DMS_ROOT_PATH, cr.dbname)
75
76     def _data_get(self, cr, uid, ids, name, arg, context=None):
77         if context is None:
78             context = {}
79         fbrl = self.browse(cr, uid, ids, context=context)
80         nctx = nodes.get_node_context(cr, uid, context={})
81         # nctx will /not/ inherit the caller's context. Most of
82         # it would be useless, anyway (like active_id, active_model,
83         # bin_size etc.)
84         result = {}
85         bin_size = context.get('bin_size', False)
86         for fbro in fbrl:
87             fnode = nodes.node_file(None, None, nctx, fbro)
88             if not bin_size:
89                     data = fnode.get_data(cr, fbro)
90                     result[fbro.id] = base64.encodestring(data or '')
91             else:
92                     result[fbro.id] = fnode.get_data_len(cr, fbro)
93
94         return result
95
96     #
97     # This code can be improved
98     #
99     def _data_set(self, cr, uid, id, name, value, arg, context=None):
100         if not value:
101             return True
102         fbro = self.browse(cr, uid, id, context=context)
103         nctx = nodes.get_node_context(cr, uid, context={})
104         fnode = nodes.node_file(None, None, nctx, fbro)
105         res = fnode.set_data(cr, base64.decodestring(value), fbro)
106         return res
107
108     _columns = {
109         # Columns from ir.attachment:
110         'create_date': fields.datetime('Date Created', readonly=True),
111         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
112         'write_date': fields.datetime('Date Modified', readonly=True),
113         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
114         'res_model': fields.char('Attached Model', size=64, readonly=True, change_default=True),
115         'res_id': fields.integer('Attached ID', readonly=True),
116
117         # If ir.attachment contained any data before document is installed, preserve
118         # the data, don't drop the column!
119         'db_datas': fields.binary('Data', oldname='datas'),
120         'datas': fields.function(_data_get, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
121
122         # Fields of document:
123         'user_id': fields.many2one('res.users', 'Owner', select=1),
124         # 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
125         # the directory id now is mandatory. It can still be computed automatically.
126         'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True, change_default=True),
127         'index_content': fields.text('Indexed Content'),
128         'partner_id':fields.many2one('res.partner', 'Partner', select=1),
129         'file_size': fields.integer('File Size', required=True),
130         'file_type': fields.char('Content Type', size=128),
131
132         # fields used for file storage
133         'store_fname': fields.char('Stored Filename', size=200),
134     }
135     _order = "id desc"
136
137     def __get_def_directory(self, cr, uid, context=None):
138         dirobj = self.pool.get('document.directory')
139         return dirobj._get_root_directory(cr, uid, context)
140
141     _defaults = {
142         'user_id': lambda self, cr, uid, ctx:uid,
143         'file_size': lambda self, cr, uid, ctx:0,
144         'parent_id': __get_def_directory
145     }
146     _sql_constraints = [
147         # filename_uniq is not possible in pure SQL
148     ]
149     def _check_duplication(self, cr, uid, vals, ids=[], op='create'):
150         name = vals.get('name', False)
151         parent_id = vals.get('parent_id', False)
152         res_model = vals.get('res_model', False)
153         res_id = vals.get('res_id', 0)
154         if op == 'write':
155             for file in self.browse(cr, uid, ids): # FIXME fields_only
156                 if not name:
157                     name = file.name
158                 if not parent_id:
159                     parent_id = file.parent_id and file.parent_id.id or False
160                 if not res_model:
161                     res_model = file.res_model and file.res_model or False
162                 if not res_id:
163                     res_id = file.res_id and file.res_id or 0
164                 res = self.search(cr, uid, [('id', '<>', file.id), ('name', '=', name), ('parent_id', '=', parent_id), ('res_model', '=', res_model), ('res_id', '=', res_id)])
165                 if len(res):
166                     return False
167         if op == 'create':
168             res = self.search(cr, uid, [('name', '=', name), ('parent_id', '=', parent_id), ('res_id', '=', res_id), ('res_model', '=', res_model)])
169             if len(res):
170                 return False
171         return True
172
173     def check(self, cr, uid, ids, mode, context=None, values=None):
174         """Check access wrt. res_model, relax the rule of ir.attachment parent
175
176         With 'document' installed, everybody will have access to attachments of
177         any resources they can *read*.
178         """
179         return super(document_file, self).check(cr, uid, ids, mode='read',
180                                             context=context, values=values)
181
182     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
183         # Grab ids, bypassing 'count'
184         ids = super(document_file, self).search(cr, uid, args, offset=offset,
185                                                 limit=limit, order=order,
186                                                 context=context, count=False)
187         if not ids:
188             return 0 if count else []
189
190         # Filter out documents that are in directories that the user is not allowed to read.
191         # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
192         # not fail), and the records have been filtered in parent's search() anyway.
193         cr.execute('SELECT id, parent_id from "%s" WHERE id in %%s' % self._table, (tuple(ids),))
194         doc_pairs = cr.fetchall()
195         parent_ids = set(zip(*doc_pairs)[1])
196         visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
197         disallowed_parents = parent_ids.difference(visible_parent_ids)
198         for doc_id, parent_id in doc_pairs:
199             if parent_id in disallowed_parents:
200                 ids.remove(doc_id)
201         return len(ids) if count else ids
202
203
204     def copy(self, cr, uid, id, default=None, context=None):
205         if not default:
206             default = {}
207         if 'name' not in default:
208             name = self.read(cr, uid, [id], ['name'])[0]['name']
209             default.update({'name': name + " " + _("(copy)")})
210         return super(document_file, self).copy(cr, uid, id, default, context=context)
211
212     def write(self, cr, uid, ids, vals, context=None):
213         result = False
214         if not isinstance(ids, list):
215             ids = [ids]
216         res = self.search(cr, uid, [('id', 'in', ids)])
217         if not len(res):
218             return False
219         if not self._check_duplication(cr, uid, vals, ids, 'write'):
220             raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
221
222         # if nodes call this write(), they must skip the code below
223         from_node = context and context.get('__from_node', False)
224         if (('parent_id' in vals) or ('name' in vals)) and not from_node:
225             # perhaps this file is renaming or changing directory
226             nctx = nodes.get_node_context(cr,uid,context={})
227             dirobj = self.pool.get('document.directory')
228             if 'parent_id' in vals:
229                 dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context)
230                 dnode = nctx.get_dir_node(cr, dbro)
231             else:
232                 dbro = None
233                 dnode = None
234             ids2 = []
235             for fbro in self.browse(cr, uid, ids, context=context):
236                 if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \
237                     and ('name' not in vals or fbro.name == vals['name']):
238                         ids2.append(fbro.id)
239                         continue
240                 fnode = nctx.get_file_node(cr, fbro)
241                 res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True)
242                 if isinstance(res, dict):
243                     vals2 = vals.copy()
244                     vals2.update(res)
245                     wid = res.get('id', fbro.id)
246                     result = super(document_file,self).write(cr,uid,wid,vals2,context=context)
247                     # TODO: how to handle/merge several results?
248                 elif res == True:
249                     ids2.append(fbro.id)
250                 elif res == False:
251                     pass
252             ids = ids2
253         if 'file_size' in vals: # only write that field using direct SQL calls
254             del vals['file_size']
255         if ids and vals:
256             result = super(document_file,self).write(cr, uid, ids, vals, context=context)
257         return result
258
259     def create(self, cr, uid, vals, context=None):
260         if context is None:
261             context = {}
262         vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
263         if not vals['parent_id']:
264             vals['parent_id'] = self.pool.get('document.directory')._get_root_directory(cr,uid, context)
265         if not vals.get('res_id', False) and context.get('default_res_id', False):
266             vals['res_id'] = context.get('default_res_id', False)
267         if not vals.get('res_model', False) and context.get('default_res_model', False):
268             vals['res_model'] = context.get('default_res_model', False)
269         if vals.get('res_id', False) and vals.get('res_model', False) \
270                 and not vals.get('partner_id', False):
271             vals['partner_id'] = self.__get_partner_id(cr, uid, \
272                 vals['res_model'], vals['res_id'], context)
273
274         datas = None
275         if vals.get('link', False) :
276             import urllib
277             datas = base64.encodestring(urllib.urlopen(vals['link']).read())
278         else:
279             datas = vals.get('datas', False)
280
281         if datas:
282             vals['file_size'] = len(datas)
283         else:
284             if vals.get('file_size'):
285                 del vals['file_size']
286         result = self._check_duplication(cr, uid, vals)
287         if not result:
288             domain = [
289                 ('res_id', '=', vals['res_id']),
290                 ('res_model', '=', vals['res_model']),
291                 ('datas_fname', '=', vals['datas_fname']),
292             ]
293             attach_ids = self.search(cr, uid, domain, context=context)
294             super(document_file, self).write(cr, uid, attach_ids, 
295                                              {'datas' : vals['datas']},
296                                              context=context)
297             result = attach_ids[0]
298         else:
299             #raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
300             result = super(document_file, self).create(cr, uid, vals, context)
301         return result
302
303     def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
304         """ A helper to retrieve the associated partner from any res_model+id
305             It is a hack that will try to discover if the mentioned record is
306             clearly associated with a partner record.
307         """
308         obj_model = self.pool.get(res_model)
309         if obj_model._name == 'res.partner':
310             return res_id
311         elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
312             bro = obj_model.browse(cr, uid, res_id, context=context)
313             return bro.partner_id.id
314         elif 'address_id' in obj_model._columns and obj_model._columns['address_id']._obj == 'res.partner.address':
315             bro = obj_model.browse(cr, uid, res_id, context=context)
316             return bro.address_id.partner_id.id
317         return False
318
319     def unlink(self, cr, uid, ids, context=None):
320         stor = self.pool.get('document.storage')
321         unres = []
322         # We have to do the unlink in 2 stages: prepare a list of actual
323         # files to be unlinked, update the db (safer to do first, can be
324         # rolled back) and then unlink the files. The list wouldn't exist
325         # after we discard the objects
326         ids = self.search(cr, uid, [('id','in',ids)])
327         for f in self.browse(cr, uid, ids, context=context):
328             # TODO: update the node cache
329             par = f.parent_id
330             storage_id = None
331             while par:
332                 if par.storage_id:
333                     storage_id = par.storage_id
334                     break
335                 par = par.parent_id
336             #assert storage_id, "Strange, found file #%s w/o storage!" % f.id #TOCHECK: after run yml, it's fail
337             if storage_id:
338                 r = stor.prepare_unlink(cr, uid, storage_id, f)
339                 if r:
340                     unres.append(r)
341             else:
342                 logging.getLogger('document').warning("Unlinking attachment #%s %s that has no storage",
343                                                 f.id, f.name)
344         res = super(document_file, self).unlink(cr, uid, ids, context)
345         stor.do_unlink(cr, uid, unres)
346         return res
347
348 document_file()
349
350
351 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: