diff --git a/CHANGELOG b/CHANGELOG
index 234e996824..03b034e1b3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Allow copying attachments and notes to created records
* Add link button on form
* Support explicit delete and remove for saving and on_change xxx2Many
* Add export_data_domain to ModelStorage
diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py
index 2061228998..f0b6ca499b 100644
--- a/trytond/ir/attachment.py
+++ b/trytond/ir/attachment.py
@@ -3,15 +3,14 @@
from sql import Null
from sql.operators import Concat
+from trytond.i18n import lazy_gettext
from ..model import ModelView, ModelSQL, fields
from ..transaction import Transaction
from ..pyson import Eval
-from .resource import ResourceMixin
+from .resource import ResourceMixin, resource_copy
from ..config import config
-__all__ = [
- 'Attachment',
- ]
+__all__ = ['AttachmentCopyMixin']
def firstline(description):
@@ -102,3 +101,10 @@ class Attachment(ResourceMixin, ModelSQL, ModelView):
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
+
+
+class AttachmentCopyMixin(
+ resource_copy(
+ 'ir.attachment', 'attachments',
+ lazy_gettext('ir.msg_attachments'))):
+ pass
diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml
index b252a7357b..653c20c303 100644
--- a/trytond/ir/message.xml
+++ b/trytond/ir/message.xml
@@ -302,5 +302,14 @@ this repository contains the full copyright notices and license terms. -->
s
+
+ The resources to which this record must be copied.
+
+
+ Attachments
+
+
+ Notes
+
diff --git a/trytond/ir/note.py b/trytond/ir/note.py
index 8f988f195a..4b821bdaa0 100644
--- a/trytond/ir/note.py
+++ b/trytond/ir/note.py
@@ -5,14 +5,15 @@ from textwrap import TextWrapper
from sql import Null
from sql.conditionals import Case
+from trytond.i18n import lazy_gettext
from ..model import ModelView, ModelSQL, ModelStorage, fields
from ..pool import Pool
from ..transaction import Transaction
from ..tools import grouped_slice, reduce_ids
from ..pyson import Eval
-from .resource import ResourceMixin
+from .resource import ResourceMixin, resource_copy
-__all__ = ['Note', 'NoteRead']
+__all__ = ['NoteCopyMixin']
class Note(ResourceMixin, ModelSQL, ModelView):
@@ -115,3 +116,8 @@ class NoteRead(ModelSQL):
ondelete='CASCADE')
user = fields.Many2One('res.user', 'User', required=True,
ondelete='CASCADE')
+
+
+class NoteCopyMixin(
+ resource_copy('ir.note', 'notes', lazy_gettext('ir.msg_notes'))):
+ pass
diff --git a/trytond/ir/resource.py b/trytond/ir/resource.py
index 4aaac956ff..c903dad04c 100644
--- a/trytond/ir/resource.py
+++ b/trytond/ir/resource.py
@@ -2,18 +2,28 @@
# this repository contains the full copyright notices and license terms.
from sql.conditionals import Coalesce
-from ..model import ModelSQL, ModelView, fields
+from trytond.i18n import lazy_gettext
+from ..model import ModelStorage, ModelView, fields
from ..pool import Pool
from ..transaction import Transaction
from ..pyson import Eval
-__all__ = ['ResourceMixin']
+__all__ = ['ResourceMixin', 'resource_copy']
-class ResourceMixin(ModelSQL, ModelView):
+class ResourceMixin(ModelStorage, ModelView):
resource = fields.Reference('Resource', selection='get_models',
required=True, select=True)
+ copy_to_resources = fields.MultiSelection(
+ 'get_copy_to_resources', "Copy to Resources",
+ states={
+ 'invisible': ~Eval('copy_to_resources_visible'),
+ },
+ depends=['copy_to_resources_visible'])
+ copy_to_resources_visible = fields.Function(
+ fields.Boolean("Copy to Resources Visible"),
+ 'on_change_with_copy_to_resources_visible')
last_user = fields.Function(fields.Char('Last User',
states={
'invisible': ~Eval('last_user'),
@@ -43,6 +53,24 @@ class ResourceMixin(ModelSQL, ModelView):
access = ModelAccess.get_access([m.model for m in models])
return [(m.model, m.name) for m in models if access[m.model]['read']]
+ @fields.depends('resource')
+ def get_copy_to_resources(self):
+ pool = Pool()
+ Model = pool.get('ir.model')
+ resources = []
+ if isinstance(self.resource, ResourceCopyMixin):
+ models = self.resource.get_resources_to_copy(self.__name__)
+ if models:
+ models = Model.search([
+ ('model', 'in', models),
+ ])
+ resources.extend((m.model, m.name) for m in models)
+ return resources
+
+ @fields.depends(methods=['get_copy_to_resources'])
+ def on_change_with_copy_to_resources_visible(self, name=None):
+ return bool(self.get_copy_to_resources())
+
def get_last_user(self, name):
return (self.write_uid.rec_name if self.write_uid
else self.create_uid.rec_name)
@@ -103,3 +131,49 @@ class ResourceMixin(ModelSQL, ModelView):
records = super(ResourceMixin, cls).create(vlist)
cls.check_access([r.id for r in records], mode='create')
return records
+
+
+class ResourceCopyMixin(ModelStorage):
+
+ @classmethod
+ def get_resources_to_copy(cls, name):
+ return set()
+
+
+def resource_copy(resource, name, string):
+
+ class _ResourceCopyMixin(ResourceCopyMixin):
+
+ @classmethod
+ def copy(cls, records, default=None):
+ if default is None:
+ default = {}
+ else:
+ default = default.copy()
+ default.setdefault(name, None)
+ return super().copy(records, default=default)
+
+ def copy_resources_to(self, target):
+ pool = Pool()
+ Resource = pool.get(resource)
+
+ try:
+ super().copy_resources_to(target)
+ except AttributeError:
+ pass
+
+ to_copy = []
+ for record in getattr(self, name):
+ if (record.copy_to_resources
+ and target.__name__ in record.copy_to_resources):
+ to_copy.append(record)
+ if to_copy:
+ return Resource.copy(to_copy, default={
+ 'resource': str(target),
+ 'copy_to_resources': None,
+ })
+
+ setattr(_ResourceCopyMixin, name, fields.One2Many(
+ resource, 'resource', string,
+ help=lazy_gettext('ir.msg_resource_copy_help')))
+ return _ResourceCopyMixin
diff --git a/trytond/ir/view/attachment_form.xml b/trytond/ir/view/attachment_form.xml
index 0a14e6e46f..a393ef19f0 100644
--- a/trytond/ir/view/attachment_form.xml
+++ b/trytond/ir/view/attachment_form.xml
@@ -16,6 +16,8 @@ this repository contains the full copyright notices and license terms. -->
+
+
diff --git a/trytond/ir/view/note_form.xml b/trytond/ir/view/note_form.xml
index a17a93c8b5..3dc18c2d53 100644
--- a/trytond/ir/view/note_form.xml
+++ b/trytond/ir/view/note_form.xml
@@ -1,14 +1,16 @@
-
diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py
index e6b3f4dcab..0d5aac7d31 100644
--- a/trytond/tests/__init__.py
+++ b/trytond/tests/__init__.py
@@ -40,6 +40,7 @@ def register():
from . import modelview
from . import mptt
from . import multivalue
+ from . import resource
from . import rule
from . import tree
from . import trigger
@@ -78,6 +79,7 @@ def register():
modelview.register('tests')
mptt.register('tests')
multivalue.register('tests')
+ resource.register('tests')
rule.register('tests')
tree.register('tests')
trigger.register('tests')
diff --git a/trytond/tests/resource.py b/trytond/tests/resource.py
new file mode 100644
index 0000000000..0bafc9c9c8
--- /dev/null
+++ b/trytond/tests/resource.py
@@ -0,0 +1,27 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+from trytond.model import ModelSQL
+from trytond.pool import Pool
+
+from trytond.ir.note import NoteCopyMixin
+
+
+class TestResource(ModelSQL, NoteCopyMixin):
+ "Test Resource"
+ __name__ = 'test.resource'
+
+ @classmethod
+ def get_resources_to_copy(cls, name):
+ return {'test.resource.other'}
+
+
+class TestResourceOther(ModelSQL):
+ "Test Resource Other"
+ __name__ = 'test.resource.other'
+
+
+def register(module):
+ Pool.register(
+ TestResource,
+ TestResourceOther,
+ module=module, type_='model')
diff --git a/trytond/tests/test_resource.py b/trytond/tests/test_resource.py
new file mode 100644
index 0000000000..d9311071b5
--- /dev/null
+++ b/trytond/tests/test_resource.py
@@ -0,0 +1,59 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+import unittest
+
+from trytond.pool import Pool
+from trytond.tests.test_tryton import activate_module, with_transaction
+
+
+class ResourceTestCase(unittest.TestCase):
+ "Test Resource"
+
+ @classmethod
+ def setUpClass(cls):
+ activate_module('tests')
+
+ @with_transaction()
+ def test_resources_copied(self):
+ "Test resources are copied"
+ pool = Pool()
+ Resource = pool.get('test.resource')
+ Other = pool.get('test.resource.other')
+ Note = pool.get('ir.note')
+
+ record = Resource()
+ record.save()
+ note = Note(resource=record, copy_to_resources=[Other.__name__])
+ note.save()
+ other = Other()
+ other.save()
+ copies = record.copy_resources_to(other)
+
+ other_notes = Note.search([('resource', '=', str(other))])
+ self.assertTrue(other_notes)
+ self.assertEqual(len(other_notes), 1)
+ self.assertEqual(other_notes, copies)
+
+ @with_transaction()
+ def test_resources_not_copied(self):
+ "Test resources are not copied"
+ pool = Pool()
+ Resource = pool.get('test.resource')
+ Other = pool.get('test.resource.other')
+ Note = pool.get('ir.note')
+
+ record = Resource()
+ record.save()
+ note = Note(resource=record)
+ note.save()
+ other = Other()
+ other.save()
+ copies = record.copy_resources_to(other)
+
+ other_notes = Note.search([('resource', '=', str(other))])
+ self.assertFalse(other_notes)
+ self.assertFalse(copies)
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(ResourceTestCase)