diff --git a/CHANGELOG b/CHANGELOG
index a288256622..f5d546bba3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add Replace Party wizard
* Remove streetbis from Address
* Add address format per country and language
* Restrict subdivision address to first level
diff --git a/__init__.py b/__init__.py
index 4ff0ea5e5c..81abb4573a 100644
--- a/__init__.py
+++ b/__init__.py
@@ -16,6 +16,7 @@ def register():
PartyCategory,
PartyIdentifier,
CheckVIESResult,
+ PartyReplaceAsk,
Address,
AddressFormat,
ContactMechanism,
@@ -23,4 +24,5 @@ def register():
module='party', type_='model')
Pool.register(
CheckVIES,
+ PartyReplace,
module='party', type_='wizard')
diff --git a/party.py b/party.py
index b0fba1862d..27e9c5ff6e 100644
--- a/party.py
+++ b/party.py
@@ -4,18 +4,19 @@ from itertools import groupby
import stdnum.eu.vat as vat
import stdnum.exceptions
-from sql import Null
+from sql import Null, Column
from sql.functions import CharLength
from trytond.model import ModelView, ModelSQL, fields, Unique
from trytond.wizard import Wizard, StateTransition, StateView, Button
-from trytond.pyson import Eval
+from trytond.pyson import Eval, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond import backend
__all__ = ['Party', 'PartyCategory', 'PartyIdentifier',
- 'CheckVIESResult', 'CheckVIES']
+ 'CheckVIESResult', 'CheckVIES',
+ 'PartyReplace', 'PartyReplaceAsk']
VAT_COUNTRIES = [('', '')]
STATES = {
@@ -48,7 +49,14 @@ class Party(ModelSQL, ModelView):
'Contact Mechanisms', states=STATES, depends=DEPENDS)
categories = fields.Many2Many('party.party-party.category',
'party', 'category', 'Categories', states=STATES, depends=DEPENDS)
- active = fields.Boolean('Active', select=True)
+ active = fields.Boolean('Active', select=True, states={
+ 'readonly': Bool(Eval('replaced_by')),
+ },
+ depends=['replaced_by'])
+ replaced_by = fields.Many2One('party.party', "Replaced By", readonly=True,
+ states={
+ 'invisible': ~Eval('replaced_by'),
+ })
full_name = fields.Function(fields.Char('Full Name'), 'get_full_name')
phone = fields.Function(fields.Char('Phone'), 'get_mechanism')
mobile = fields.Function(fields.Char('Mobile'), 'get_mechanism')
@@ -388,3 +396,115 @@ class CheckVIES(Wizard):
'parties_succeed': [p.id for p in self.result.parties_succeed],
'parties_failed': [p.id for p in self.result.parties_failed],
}
+
+
+class PartyReplace(Wizard):
+ "Replace Party"
+ __name__ = 'party.replace'
+ start_state = 'ask'
+ ask = StateView('party.replace.ask', 'party.replace_ask_view_form', [
+ Button("Cancel", 'end', 'tryton-cancel'),
+ Button("Replace", 'replace', 'tryton-find-replace', default=True),
+ ])
+ replace = StateTransition()
+
+ @classmethod
+ def __setup__(cls):
+ super(PartyReplace, cls).__setup__()
+ cls._error_messages.update({
+ 'different_name': ("Parties have different names: "
+ "%(source_name)s vs %(destination_name)s."),
+ 'different_vat_code': ("Parties have different VAT Code: "
+ "%(source_code)s vs %(destination_code)s."),
+ })
+
+ def check_similarity(self):
+ source = self.ask.source
+ destination = self.ask.destination
+ if source.name != destination.name:
+ key = 'party.replace name %s %s' % (source.id, destination.id)
+ self.raise_user_warning(key, 'different_name', {
+ 'source_name': source.name,
+ 'destination_name': destination.name,
+ })
+ if source.vat_code != destination.vat_code:
+ key = 'party.replace vat_code %s %s' % (source.id, destination.id)
+ self.raise_user_warning(key, 'different_vat_code', {
+ 'source_code': source.vat_code,
+ 'destination_code': destination.vat_code,
+ })
+
+ def transition_replace(self):
+ pool = Pool()
+ Address = pool.get('party.address')
+ ContactMechanism = pool.get('party.contact_mechanism')
+ transaction = Transaction()
+
+ self.check_similarity()
+ source = self.ask.source
+ destination = self.ask.destination
+
+ Address.write(list(source.addresses), {
+ 'active': False,
+ })
+ ContactMechanism.write(list(source.contact_mechanisms), {
+ 'active': False,
+ })
+ source.replaced_by = destination
+ source.active = False
+ source.save()
+
+ cursor = transaction.connection.cursor()
+ for model_name, field_name in self.fields_to_replace():
+ Model = pool.get(model_name)
+ table = Model.__table__()
+ column = Column(table, field_name)
+ where = column == source.id
+
+ if transaction.database.has_returning():
+ returning = [table.id]
+ else:
+ cursor.execute(*table.select(table.id, where=where))
+ ids = [x[0] for x in cursor]
+ returning = None
+
+ cursor.execute(*table.update(
+ [column],
+ [destination.id],
+ where=where,
+ returning=returning))
+
+ if transaction.database.has_returning():
+ ids = [x[0] for x in cursor]
+
+ Model._insert_history(ids)
+ return 'end'
+
+ @classmethod
+ def fields_to_replace(cls):
+ return [
+ ('party.address', 'party'),
+ ('party.contact_mechanism', 'party'),
+ ]
+
+
+class PartyReplaceAsk(ModelView):
+ "Replace Party"
+ __name__ = 'party.replace.ask'
+ source = fields.Many2One('party.party', "Source", required=True)
+ destination = fields.Many2One('party.party', "Destination", required=True,
+ domain=[
+ ('id', '!=', Eval('source', -1)),
+ ],
+ depends=['source'])
+
+ @classmethod
+ def default_source(cls):
+ context = Transaction().context
+ if context.get('active_model') == 'party.party':
+ return context.get('active_id')
+
+ @fields.depends('source')
+ def on_change_source(self):
+ if self.source and self.source.replaced_by:
+ self.destination = self.source.replaced_by
diff --git a/party.xml b/party.xml
index add60098b8..2d16b35b7a 100644
--- a/party.xml
+++ b/party.xml
@@ -133,5 +133,26 @@ this repository contains the full copyright notices and license terms. -->
check_vies_result
+
+ Replace
+ party.replace
+ party.party
+
+
+
+
+
+
+ form_action
+ party.party,-1
+
+
+
+
+ party.replace.ask
+ form
+ replace_ask_form
+
diff --git a/setup.py b/setup.py
index 1b110b6959..4163a00f29 100644
--- a/setup.py
+++ b/setup.py
@@ -53,6 +53,12 @@ for dep in info.get('depends', []):
requires.append(get_require_version('trytond_%s' % dep))
requires.append(get_require_version('trytond'))
+tests_require = [get_require_version('proteus')]
+dependency_links = []
+if minor_version % 2:
+ # Add development index for testing with proteus
+ dependency_links.append('https://trydevpi.tryton.org/')
+
setup(name=name,
version=version,
description='Tryton module with parties and addresses',
@@ -70,7 +76,7 @@ setup(name=name,
package_data={
'trytond.modules.party': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po', '*.odt',
- 'icons/*.svg']),
+ 'icons/*.svg', 'tests/*.rst']),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -116,5 +122,9 @@ setup(name=name,
""",
test_suite='tests',
test_loader='trytond.test_loader:Loader',
+ tests_require=tests_require,
use_2to3=True,
+ convert_2to3_doctests=[
+ 'tests/scenario_party_replace.rst',
+ ],
)
diff --git a/tests/scenario_party_replace.rst b/tests/scenario_party_replace.rst
new file mode 100644
index 0000000000..296f8db4f1
--- /dev/null
+++ b/tests/scenario_party_replace.rst
@@ -0,0 +1,43 @@
+======================
+Party Replace Scenario
+======================
+
+Imports::
+
+ >>> from proteus import Model, Wizard
+ >>> from trytond.tests.tools import install_modules
+
+Install party::
+
+ >>> config = install_modules('party')
+
+Create a party::
+
+ >>> Party = Model.get('party.party')
+ >>> party1 = Party(name='Pam')
+ >>> party1.save()
+ >>> address1, = party1.addresses
+
+Create a second party similar party::
+
+ >>> party2 = Party(name='Pam')
+ >>> party2.save()
+ >>> address2, = party2.addresses
+
+Replace the second by the first party::
+
+ >>> replace = Wizard('party.replace', models=[party2])
+ >>> replace.form.source == party2
+ True
+ >>> replace.form.destination = party1
+ >>> replace.execute('replace')
+
+ >>> party2.reload()
+ >>> bool(party2.active)
+ False
+
+ >>> address2.reload()
+ >>> address2.party == party1
+ True
+ >>> bool(address2.active)
+ False
diff --git a/tests/test_party.py b/tests/test_party.py
index 7bca42bf39..79d49a96f6 100644
--- a/tests/test_party.py
+++ b/tests/test_party.py
@@ -1,8 +1,12 @@
# 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
+import doctest
+
import trytond.tests.test_tryton
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
+from trytond.tests.test_tryton import doctest_teardown
+from trytond.tests.test_tryton import doctest_checker
from trytond.pool import Pool
from trytond.transaction import Transaction
@@ -121,4 +125,9 @@ class PartyTestCase(ModuleTestCase):
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(PartyTestCase))
+ suite.addTests(doctest.DocFileSuite(
+ 'scenario_party_replace.rst',
+ tearDown=doctest_teardown, encoding='utf-8',
+ checker=doctest_checker,
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite
diff --git a/view/party_form.xml b/view/party_form.xml
index 433377d918..2ef4069b41 100644
--- a/view/party_form.xml
+++ b/view/party_form.xml
@@ -11,6 +11,8 @@ this repository contains the full copyright notices and license terms. -->
+
+
+