@@ -50,6 +50,7 @@ def make_index_name(table_name, column_name):
50
50
from .misc import SelfPrintEvalContext , log_progress , version_gte
51
51
from .orm import env , invalidate
52
52
from .pg import (
53
+ SQLStr ,
53
54
alter_column_type ,
54
55
column_exists ,
55
56
column_type ,
@@ -182,7 +183,7 @@ def filter_value(key, value):
182
183
return changed
183
184
184
185
185
- def remove_field (cr , model , fieldname , cascade = False , drop_column = True , skip_inherit = ()):
186
+ def remove_field (cr , model , fieldname , cascade = False , drop_column = True , skip_inherit = (), keep_as_attachments = False ):
186
187
"""
187
188
Remove a field and its references from the database.
188
189
@@ -196,6 +197,8 @@ def remove_field(cr, model, fieldname, cascade=False, drop_column=True, skip_inh
196
197
:param bool drop_column: whether the field's column is dropped
197
198
:param list(str) or str skip_inherit: list of inheriting models to skip the removal
198
199
of the field, use `"*"` to skip all
200
+ :param bool keep_as_attachments: for binary fields, whether the data should be kept
201
+ as attachments
199
202
"""
200
203
_validate_model (model )
201
204
@@ -353,8 +356,20 @@ def adapter(leaf, is_or, negated):
353
356
defaults .pop (fieldname , None )
354
357
cr .execute ("UPDATE mail_alias SET alias_defaults = %s WHERE id = %s" , [repr (defaults ), alias_id ])
355
358
356
- # if field was a binary field stored as attachment, clean them...
357
- if column_exists (cr , "ir_attachment" , "res_field" ):
359
+ table = table_of_model (cr , model )
360
+ # NOTE table_exists is needed to avoid altering views
361
+ stored = table_exists (cr , table ) and column_exists (cr , table , fieldname )
362
+
363
+ if keep_as_attachments :
364
+ if stored and column_type (cr , table , fieldname ) == "bytea" :
365
+ _extract_data_as_attachment (cr , model , fieldname , set_res_field = False )
366
+ if column_exists (cr , "ir_attachment" , "res_field" ):
367
+ query = cr .mogrify (
368
+ "UPDATE ir_attachment SET res_field = NULL WHERE res_model = %s AND res_field = %s" , [model , fieldname ]
369
+ ).decode ()
370
+ explode_execute (cr , query , table = "ir_attachment" )
371
+
372
+ elif column_exists (cr , "ir_attachment" , "res_field" ):
358
373
parallel_execute (
359
374
cr ,
360
375
explode_query_range (
@@ -367,13 +382,20 @@ def adapter(leaf, is_or, negated):
367
382
)
368
383
369
384
table = table_of_model (cr , model )
370
- # NOTE table_exists is needed to avoid altering views
371
- if drop_column and table_exists (cr , table ) and column_exists (cr , table , fieldname ):
385
+ if drop_column and stored :
372
386
remove_column (cr , table , fieldname , cascade = cascade )
373
387
374
388
# remove field on inherits
375
389
for inh in for_each_inherit (cr , model , skip_inherit ):
376
- remove_field (cr , inh .model , fieldname , cascade = cascade , drop_column = drop_column , skip_inherit = skip_inherit )
390
+ remove_field (
391
+ cr ,
392
+ inh .model ,
393
+ fieldname ,
394
+ cascade = cascade ,
395
+ drop_column = drop_column ,
396
+ skip_inherit = skip_inherit ,
397
+ keep_as_attachments = keep_as_attachments ,
398
+ )
377
399
378
400
379
401
def make_field_non_stored (cr , model , field , selectable = False ):
@@ -884,18 +906,45 @@ def _convert_field_to_property(
884
906
885
907
886
908
def convert_binary_field_to_attachment (cr , model , field , encoded = True , name_field = None ):
909
+ _extract_data_as_attachment (cr , model , field , encoded , name_field )
910
+ # free PG space
911
+ table = table_of_model (cr , model )
912
+ remove_column (cr , table , field )
913
+
914
+
915
+ def _extract_data_as_attachment (cr , model , field , encoded = True , name_field = None , set_res_field = True ):
887
916
_validate_model (model )
888
917
table = table_of_model (cr , model )
889
918
if not column_exists (cr , table , field ):
890
919
return
891
- name_query = "COALESCE({0}, '{1}('|| id || ').{2}')" .format (
892
- name_field if name_field else "NULL" ,
893
- model .title ().replace ("." , "" ),
894
- field ,
920
+ name_query = cr .mogrify (
921
+ format_query (cr , "COALESCE({}, CONCAT(%s, '(', id, ').', %s)" , name_field if name_field else SQLStr ("NULL" )),
922
+ [model .title ().replace ("." , "" ), field ],
923
+ ).decode ()
924
+
925
+ if not column_exists (cr , "ir_attachment" , "res_field" ):
926
+ res_field_query = SQLStr ("" )
927
+ elif set_res_field :
928
+ res_field_query = SQLStr (cr .mogrify (", res_field = %s" , [field ]).decode ())
929
+ else :
930
+ res_field_query = SQLStr (", res_field = NULL" )
931
+
932
+ update_query = format_query (
933
+ cr ,
934
+ """
935
+ UPDATE ir_attachment
936
+ SET res_model = %s,
937
+ res_id = %s
938
+ {}
939
+ WHERE id = %s
940
+ """ ,
941
+ res_field_query ,
895
942
)
896
943
897
944
cr .execute (format_query (cr , "SELECT count(*) FROM {} WHERE {} IS NOT NULL" , table , field ))
898
945
[count ] = cr .fetchone ()
946
+ if count == 0 :
947
+ return
899
948
900
949
A = env (cr )["ir.attachment" ]
901
950
iter_cur = cr ._cnx .cursor ("fetch_binary" )
@@ -920,21 +969,11 @@ def convert_binary_field_to_attachment(cr, model, field, encoded=True, name_fiel
920
969
if not encoded :
921
970
data = base64 .b64encode (data ) # noqa: PLW2901
922
971
att = A .create ({"name" : name , "datas" : data , "type" : "binary" })
923
- cr .execute (
924
- """
925
- UPDATE ir_attachment
926
- SET res_model = %s,
927
- res_id = %s,
928
- res_field = %s
929
- WHERE id = %s
930
- """ ,
931
- [model , rid , field , att .id ],
932
- )
972
+
973
+ cr .execute (update_query , [model , rid , att .id ])
933
974
invalidate (att )
934
975
935
976
iter_cur .close ()
936
- # free PG space
937
- remove_column (cr , table , field )
938
977
939
978
940
979
if version_gte ("16.0" ):
0 commit comments