Launchpad automatic translations update.
[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     def _get_filestore(self, cr):
39         return os.path.join(DMS_ROOT_PATH, cr.dbname)
40
41     def _data_get(self, cr, uid, ids, name, arg, context=None):
42         if context is None:
43             context = {}
44         fbrl = self.browse(cr, uid, ids, context=context)
45         nctx = nodes.get_node_context(cr, uid, context={})
46         # nctx will /not/ inherit the caller's context. Most of
47         # it would be useless, anyway (like active_id, active_model,
48         # bin_size etc.)
49         result = {}
50         bin_size = context.get('bin_size', False)
51         for fbro in fbrl:
52             fnode = nodes.node_file(None, None, nctx, fbro)
53             if not bin_size:
54                     data = fnode.get_data(cr, fbro)
55                     result[fbro.id] = base64.encodestring(data or '')
56             else:
57                     result[fbro.id] = fnode.get_data_len(cr, fbro)
58
59         return result
60
61     #
62     # This code can be improved
63     #
64     def _data_set(self, cr, uid, id, name, value, arg, context=None):
65         if not value:
66             return True
67         fbro = self.browse(cr, uid, id, context=context)
68         nctx = nodes.get_node_context(cr, uid, context={})
69         fnode = nodes.node_file(None, None, nctx, fbro)
70         res = fnode.set_data(cr, base64.decodestring(value), fbro)
71         return res
72
73     _columns = {
74         # Columns from ir.attachment:
75         'create_date': fields.datetime('Date Created', readonly=True),
76         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
77         'write_date': fields.datetime('Date Modified', readonly=True),
78         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
79         'res_model': fields.char('Attached Model', size=64, readonly=True, change_default=True),
80         'res_id': fields.integer('Attached ID', readonly=True),
81
82         # If ir.attachment contained any data before document is installed, preserve
83         # the data, don't drop the column!
84         'db_datas': fields.binary('Data', oldname='datas'),
85         'datas': fields.function(_data_get, method=True, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
86
87         # Fields of document:
88         'user_id': fields.many2one('res.users', 'Owner', select=1),
89         # 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
90         # the directory id now is mandatory. It can still be computed automatically.
91         'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True, change_default=True),
92         'index_content': fields.text('Indexed Content'),
93         'partner_id':fields.many2one('res.partner', 'Partner', select=1),
94         'file_size': fields.integer('File Size', required=True),
95         'file_type': fields.char('Content Type', size=128),
96
97         # fields used for file storage
98         'store_fname': fields.char('Stored Filename', size=200),
99     }
100     _order = "create_date desc"
101
102     def __get_def_directory(self, cr, uid, context=None):
103         dirobj = self.pool.get('document.directory')
104         return dirobj._get_root_directory(cr, uid, context)
105
106     _defaults = {
107         'user_id': lambda self, cr, uid, ctx:uid,
108         'file_size': lambda self, cr, uid, ctx:0,
109         'parent_id': __get_def_directory
110     }
111     _sql_constraints = [
112         # filename_uniq is not possible in pure SQL
113     ]
114     def _check_duplication(self, cr, uid, vals, ids=[], op='create'):
115         name = vals.get('name', False)
116         parent_id = vals.get('parent_id', False)
117         res_model = vals.get('res_model', False)
118         res_id = vals.get('res_id', 0)
119         if op == 'write':
120             for file in self.browse(cr, uid, ids): # FIXME fields_only
121                 if not name:
122                     name = file.name
123                 if not parent_id:
124                     parent_id = file.parent_id and file.parent_id.id or False
125                 if not res_model:
126                     res_model = file.res_model and file.res_model or False
127                 if not res_id:
128                     res_id = file.res_id and file.res_id or 0
129                 res = self.search(cr, uid, [('id', '<>', file.id), ('name', '=', name), ('parent_id', '=', parent_id), ('res_model', '=', res_model), ('res_id', '=', res_id)])
130                 if len(res):
131                     return False
132         if op == 'create':
133             res = self.search(cr, uid, [('name', '=', name), ('parent_id', '=', parent_id), ('res_id', '=', res_id), ('res_model', '=', res_model)])
134             if len(res):
135                 return False
136         return True
137
138     def copy(self, cr, uid, id, default=None, context=None):
139         if not default:
140             default = {}
141         if 'name' not in default:
142             name = self.read(cr, uid, [id])[0]['name']
143             default.update({'name': name + " (copy)"})
144         return super(document_file, self).copy(cr, uid, id, default, context=context)
145
146     def write(self, cr, uid, ids, vals, context=None):
147         result = False
148         if not isinstance(ids, list):
149             ids = [ids]
150         res = self.search(cr, uid, [('id', 'in', ids)])
151         if not len(res):
152             return False
153         if not self._check_duplication(cr, uid, vals, ids, 'write'):
154             raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
155
156         # if nodes call this write(), they must skip the code below
157         from_node = context and context.get('__from_node', False)
158         if (('parent_id' in vals) or ('name' in vals)) and not from_node:
159             # perhaps this file is renaming or changing directory
160             nctx = nodes.get_node_context(cr,uid,context={})
161             dirobj = self.pool.get('document.directory')
162             if 'parent_id' in vals:
163                 dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context)
164                 dnode = nctx.get_dir_node(cr, dbro)
165             else:
166                 dbro = None
167                 dnode = None
168             ids2 = []
169             for fbro in self.browse(cr, uid, ids, context=context):
170                 if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \
171                     and ('name' not in vals or fbro.name == vals['name']) :
172                         ids2.append(fbro.id)
173                         continue
174                 fnode = nctx.get_file_node(cr, fbro)
175                 res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True)
176                 if isinstance(res, dict):
177                     vals2 = vals.copy()
178                     vals2.update(res)
179                     wid = res.get('id', fbro.id)
180                     result = super(document_file,self).write(cr,uid,wid,vals2,context=context)
181                     # TODO: how to handle/merge several results?
182                 elif res == True:
183                     ids2.append(fbro.id)
184                 elif res == False:
185                     pass
186             ids = ids2
187         if 'file_size' in vals: # only write that field using direct SQL calls
188             del vals['file_size']
189         if len(ids) and len(vals):
190             result = super(document_file,self).write(cr, uid, ids, vals, context=context)
191         cr.commit() # ?
192         return result
193
194     def create(self, cr, uid, vals, context=None):
195         if context is None:
196             context = {}
197         vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
198         if not vals['parent_id']:
199             vals['parent_id'] = self.pool.get('document.directory')._get_root_directory(cr,uid, context)
200         if not vals.get('res_id', False) and context.get('default_res_id', False):
201             vals['res_id'] = context.get('default_res_id', False)
202         if not vals.get('res_model', False) and context.get('default_res_model', False):
203             vals['res_model'] = context.get('default_res_model', False)
204         if vals.get('res_id', False) and vals.get('res_model', False) \
205                 and not vals.get('partner_id', False):
206             vals['partner_id'] = self.__get_partner_id(cr, uid, \
207                 vals['res_model'], vals['res_id'], context)
208
209         datas = None
210         if vals.get('link', False) :
211             import urllib
212             datas = base64.encodestring(urllib.urlopen(vals['link']).read())
213         else:
214             datas = vals.get('datas', False)
215
216         if datas:
217             vals['file_size'] = len(datas)
218         else:
219             if vals.get('file_size'):
220                 del vals['file_size']
221         if not self._check_duplication(cr, uid, vals):
222             raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
223         result = super(document_file, self).create(cr, uid, vals, context)
224         cr.commit() # ?
225         return result
226
227     def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
228         """ A helper to retrieve the associated partner from any res_model+id
229             It is a hack that will try to discover if the mentioned record is
230             clearly associated with a partner record.
231         """
232         obj_model = self.pool.get(res_model)
233         if obj_model._name == 'res.partner':
234             return res_id
235         elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
236             bro = obj_model.browse(cr, uid, res_id, context=context)
237             return bro.partner_id.id
238         elif 'address_id' in obj_model._columns and obj_model._columns['address_id']._obj == 'res.partner.address':
239             bro = obj_model.browse(cr, uid, res_id, context=context)
240             return bro.address_id.partner_id.id
241         return False
242
243     def unlink(self, cr, uid, ids, context=None):
244         stor = self.pool.get('document.storage')
245         unres = []
246         # We have to do the unlink in 2 stages: prepare a list of actual
247         # files to be unlinked, update the db (safer to do first, can be
248         # rolled back) and then unlink the files. The list wouldn't exist
249         # after we discard the objects
250         ids = self.search(cr, uid, [('id','in',ids)])
251         for f in self.browse(cr, uid, ids, context=context):
252             # TODO: update the node cache
253             par = f.parent_id
254             storage_id = None
255             while par:
256                 if par.storage_id:
257                     storage_id = par.storage_id
258                     break
259                 par = par.parent_id
260             #assert storage_id, "Strange, found file #%s w/o storage!" % f.id #TOCHECK: after run yml, it's fail
261             if storage_id:
262                 r = stor.prepare_unlink(cr, uid, storage_id, f)
263                 if r:
264                     unres.append(r)
265             else:
266                 logging.getLogger('document').warning("Unlinking attachment #%s %s that has no storage",
267                                                 f.id, f.name)
268         res = super(document_file, self).unlink(cr, uid, ids, context)
269         stor.do_unlink(cr, uid, unres)
270         return res
271
272 document_file()
273