From 3cd9694f5fed2262a719bb395d9fa7159e70a1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 2 Oct 2016 15:18:38 +0200 Subject: [PATCH] Add Replace Party wizard issue5820 review26581003 --- CHANGELOG | 1 + __init__.py | 2 + party.py | 128 ++++++++++++++++++++++++++++++- party.xml | 21 +++++ setup.py | 12 ++- tests/scenario_party_replace.rst | 43 +++++++++++ tests/test_party.py | 9 +++ view/party_form.xml | 2 + view/replace_ask_form.xml | 7 ++ 9 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 tests/scenario_party_replace.rst create mode 100644 view/replace_ask_form.xml 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. --> +