From a50a7e96b5f99689524394c9711cacbcfa7ff058 Mon Sep 17 00:00:00 2001 From: Albert Cervera i Areny Date: Thu, 5 Feb 2015 19:20:31 +0100 Subject: [PATCH] Initial commit. Original module from openlabs with some improvements. --- Makefile | 16 ++++ __init__.py | 22 +++++ locale/de_DE.po | 35 ++++++++ party.py | 122 +++++++++++++++++++++++++++ party.xml | 25 ++++++ requirements.txt | 4 + setup.cfg | 6 ++ setup.py | 145 +++++++++++++++++++++++++++++++++ tests/__init__.py | 28 +++++++ tests/test_party.py | 87 ++++++++++++++++++++ tests/test_views_depends.py | 57 +++++++++++++ tryton.cfg | 6 ++ view/party_form.xml | 11 +++ view/party_merge_view_form.xml | 5 ++ 14 files changed, 569 insertions(+) create mode 100644 Makefile create mode 100644 __init__.py create mode 100644 locale/de_DE.po create mode 100644 party.py create mode 100644 party.xml create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_party.py create mode 100644 tests/test_views_depends.py create mode 100644 tryton.cfg create mode 100644 view/party_form.xml create mode 100644 view/party_merge_view_form.xml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c5784d --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +test: test-sqlite test-postgres test-flake8 + +test-sqlite: install-dependencies + coverage run setup.py test + coverage report -m --fail-under 80 + +test-postgres: install-dependencies + python setup.py test_on_postgres + +test-flake8: + pip install flake8 + flake8 . + +install-dependencies: + CFLAGS=-O0 pip install lxml + pip install -r dev_requirements.txt diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d9d09a2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" + __init__.py + + :copyright: (c) 2014 by Openlabs Technologies & Consulting (P) Limited + :license: BSD, see LICENSE for more details. +""" +from trytond.pool import Pool + +from party import Party, PartyMergeView, PartyMerge + + +def register(): + Pool.register( + Party, + PartyMergeView, + module='party_merge', type_='model' + ) + Pool.register( + PartyMerge, + module='party_merge', type_='wizard' + ) diff --git a/locale/de_DE.po b/locale/de_DE.po new file mode 100644 index 0000000..6cc486c --- /dev/null +++ b/locale/de_DE.po @@ -0,0 +1,35 @@ +# +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgctxt "field:party.party.merge.view,duplicates:" +msgid "Duplicates" +msgstr "Duplikate" + +msgctxt "field:party.party.merge.view,id:" +msgid "ID" +msgstr "ID" + +msgctxt "field:party.party.merge.view,target:" +msgid "Target" +msgstr "Ziel" + +msgctxt "model:ir.action,name:wizard_party_merge" +msgid "Merge Parties" +msgstr "Parteien zusammenfassen" + +msgctxt "model:party.party.merge.view,name:" +msgid "Party Merge" +msgstr "Parteien zusammenfassen" + +msgctxt "view:party.party.merge.view:" +msgid "Merge Parties" +msgstr "Parteien zusammenfassen" + +msgctxt "wizard_button:party.party.merge,merge,end:" +msgid "Cancel" +msgstr "Abbrechen" + +msgctxt "wizard_button:party.party.merge,merge,result:" +msgid "OK" +msgstr "OK" diff --git a/party.py b/party.py new file mode 100644 index 0000000..0e8c1c7 --- /dev/null +++ b/party.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" + party.py + + :copyright: (c) 2014 by Openlabs Technologies & Consulting (P) Limited + :license: BSD, see LICENSE for more details. +""" +from sql import Table +from trytond.model import ModelSQL, ModelView, fields +from trytond.transaction import Transaction +from trytond.pool import PoolMeta, Pool +from trytond.pyson import Eval, Bool, Not +from trytond.wizard import Wizard, StateView, StateTransition, Button + +__metaclass__ = PoolMeta +__all__ = ['Party', 'PartyMergeView', 'PartyMerge'] + + +class MergeMixin: + def merge_into(self, target): + """ + Merge current record into target + """ + ModelField = Pool().get('ir.model.field') + + model_fields = ModelField.search([ + ('relation', '=', self.__name__), + ('ttype', '=', 'many2one'), + ]) + + if hasattr(self, 'active'): + self.active = False + self.merged_into = target + self.save() + + cursor = Transaction().cursor + + to_validate = [] + for field in model_fields: + Model = Pool().get(field.model.model) + + if isinstance(getattr(Model, field.name), fields.Function): + continue + + if not hasattr(Model, '__table__'): + continue + + sql_table = Model.__table__() + # Discard sql.Union or others generated by table_query() + if not isinstance(sql_table, Table): + continue + + to_validate.append(field) + sql_field = getattr(sql_table, field.name) + cursor.execute(*sql_table.update( + columns=[sql_field], + values=[target.id], + where=(sql_field == self.id) + )) + + + # Validate all related records and target. + # Do it at the very end because we may # temporarily leave + # information inconsistent in the previous loop + for field in model_fields: + Model = Pool().get(field.model.model) + + if not isinstance(Model, ModelSQL): + continue + + ff = getattr(Model, field.name) + if isinstance(ff, fields.Function) and not ff.searcher: + continue + + with Transaction().set_context(active_test=False): + Model.validate(Model.search([ + (field.name, '=', target.id), + ])) + + self.validate([target]) + + +class Party(MergeMixin): + __name__ = 'party.party' + merged_into = fields.Many2One('party.party', 'Merged Into', readonly=True, + states={ + 'invisible': Not(Bool(Eval('merged_into'))), + }) + + +class PartyMergeView(ModelView): + 'Party Merge' + __name__ = 'party.party.merge.view' + + duplicates = fields.One2Many('party.party', None, 'Duplicates', + readonly=True) + target = fields.Many2One('party.party', 'Target', required=True, + domain=[('id', 'not in', Eval('duplicates'))], depends=['duplicates']) + + +class PartyMerge(Wizard): + __name__ = 'party.party.merge' + start_state = 'merge' + + merge = StateView( + 'party.party.merge.view', + 'party_merge.party_merge_view', [ + Button('Cancel', 'end', 'tryton-cancel'), + Button('OK', 'result', 'tryton-ok'), + ] + ) + result = StateTransition() + + def default_merge(self, fields): + return { + 'duplicates': Transaction().context['active_ids'], + } + + def transition_result(self): + for party in self.merge.duplicates: + party.merge_into(self.merge.target) + return 'end' diff --git a/party.xml b/party.xml new file mode 100644 index 0000000..33c0461 --- /dev/null +++ b/party.xml @@ -0,0 +1,25 @@ + + + + + party.party.merge.view + form + party_merge_view_form + + + Merge Parties + party.party.merge + + + form_action + party.party,-1 + + + + party.party + form + party_form + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99d1761 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +psycopg2 + +# Install this package itself +. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..321c3f1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[flake8] +exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,build,dist,upload.py,doc,scripts,selenium*,proteus*,Flask*,Genshi*,lxml*,relatorio*,trytond-*,docs,blinker-*,*.egg,.tox + + +max-complexity=10 +max-line-length=80 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4bc5751 --- /dev/null +++ b/setup.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +import re +import os +import sys +import time +import unittest +import ConfigParser +from setuptools import setup, Command + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +class SQLiteTest(Command): + """ + Run the tests on SQLite + """ + description = "Run tests on SQLite" + + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + from trytond.config import CONFIG + CONFIG['db_type'] = 'sqlite' + os.environ['DB_NAME'] = ':memory:' + + from tests import suite + test_result = unittest.TextTestRunner(verbosity=3).run(suite()) + + if test_result.wasSuccessful(): + sys.exit(0) + sys.exit(-1) + + +class PostgresTest(Command): + """ + Run the tests on Postgres. + """ + description = "Run tests on Postgresql" + + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + from trytond.config import CONFIG + CONFIG['db_type'] = 'postgresql' + CONFIG['db_host'] = 'localhost' + CONFIG['db_port'] = 5432 + CONFIG['db_user'] = 'postgres' + + os.environ['DB_NAME'] = 'test_' + str(int(time.time())) + + from tests import suite + test_result = unittest.TextTestRunner(verbosity=3).run(suite()) + + if test_result.wasSuccessful(): + sys.exit(0) + sys.exit(-1) + + +config = ConfigParser.ConfigParser() +config.readfp(open('tryton.cfg')) +info = dict(config.items('tryton')) +for key in ('depends', 'extras_depend', 'xml'): + if key in info: + info[key] = info[key].strip().splitlines() +major_version, minor_version, _ = info.get('version', '0.0.1').split('.', 2) +major_version = int(major_version) +minor_version = int(minor_version) + +requires = [] + +MODULE2PREFIX = {} + +MODULE = "party_merge" +PREFIX = "openlabs" +for dep in info.get('depends', []): + if not re.match(r'(ir|res|webdav)(\W|$)', dep): + requires.append( + '%s_%s >= %s.%s, < %s.%s' % ( + MODULE2PREFIX.get(dep, 'trytond'), dep, + major_version, minor_version, major_version, + minor_version + 1 + ) + ) +requires.append( + 'trytond >= %s.%s, < %s.%s' % ( + major_version, minor_version, major_version, minor_version + 1 + ) +) +setup( + name='%s_%s' % (PREFIX, MODULE), + version=info.get('version', '0.0.1'), + description="Merge Parties", + author="Openlabs Technologies and Consulting (P) Ltd.", + author_email='info@openlabs.co.in', + url='http://www.openlabs.co.in/', + package_dir={'trytond.modules.%s' % MODULE: '.'}, + packages=[ + 'trytond.modules.%s' % MODULE, + 'trytond.modules.%s.tests' % MODULE, + ], + package_data={ + 'trytond.modules.%s' % MODULE: info.get('xml', []) + + info.get('translation', []) + + ['tryton.cfg', 'locale/*.po', 'tests/*.rst', 'reports/*.odt'] + + ['view/*.xml'], + }, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Plugins', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Framework :: Tryton', + 'Topic :: Office/Business', + ], + license='GPL-3', + install_requires=requires, + zip_safe=False, + entry_points=""" + [trytond.modules] + %s = trytond.modules.%s + """ % (MODULE, MODULE), + test_suite='tests', + test_loader='trytond.test_loader:Loader', + cmdclass={ + 'test': SQLiteTest, + 'test_on_postgres': PostgresTest, + } +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a48c78d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + tests/__init__.py + + :copyright: (c) 2014 by Openlabs Technologies & Consulting (P) Limited + :license: BSD, see LICENSE for more details. +""" +import unittest + +import trytond.tests.test_tryton + +from tests.test_views_depends import TestViewsDepends +from tests.test_party import TestParty + + +def suite(): + """ + Define suite + """ + test_suite = trytond.tests.test_tryton.suite() + test_suite.addTests([ + unittest.TestLoader().loadTestsFromTestCase(TestViewsDepends), + unittest.TestLoader().loadTestsFromTestCase(TestParty), + ]) + return test_suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/tests/test_party.py b/tests/test_party.py new file mode 100644 index 0000000..970bc1c --- /dev/null +++ b/tests/test_party.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" + tests/test_party.py + + :copyright: (C) 2014 by Openlabs Technologies & Consulting (P) Limited + :license: BSD, see LICENSE for more details. +""" +import sys +import os +DIR = os.path.abspath(os.path.normpath(os.path.join( + __file__, '..', '..', '..', '..', '..', 'trytond' +))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) +import unittest + +if 'DB_NAME' not in os.environ: + from trytond.config import CONFIG + CONFIG['db_type'] = 'sqlite' + os.environ['DB_NAME'] = ':memory:' + +from trytond.tests.test_tryton import POOL, USER +from trytond.tests.test_tryton import DB_NAME, CONTEXT +from trytond.transaction import Transaction +import trytond.tests.test_tryton + + +class TestParty(unittest.TestCase): + ''' + Test Party + ''' + + def setUp(self): + """ + Set up data used in the tests. + this method is called before each test function execution. + """ + trytond.tests.test_tryton.install_module('party_merge') + + self.Party = POOL.get('party.party') + + def test0005_merge_parties(self): + """Test party merge function. + """ + with Transaction().start(DB_NAME, USER, context=CONTEXT): + party1, party2, party3 = self.Party.create([{ + 'name': 'Party 1', + 'addresses': [('create', [{ + 'name': 'party1', + 'street': 'ST2', + 'city': 'New Delhi', + }])] + }, { + 'name': 'Party 2', + 'addresses': [('create', [{ + 'name': 'party2', + 'street': 'ST2', + 'city': 'Mumbai', + }])] + }, { + 'name': 'Party 3', + 'addresses': [('create', [{ + 'name': 'party3', + 'street': 'ST2', + 'city': 'New Delhi', + }])] + }]) + + # Merge party2, party3 to party1 + party2.merge_into(party1) + party3.merge_into(party1) + + self.assertEqual(len(party1.addresses), 3) + + +def suite(): + """ + Define suite + """ + test_suite = trytond.tests.test_tryton.suite() + test_suite.addTests( + unittest.TestLoader().loadTestsFromTestCase(TestParty) + ) + return test_suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/tests/test_views_depends.py b/tests/test_views_depends.py new file mode 100644 index 0000000..3c49cc9 --- /dev/null +++ b/tests/test_views_depends.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +""" + tests/test_views_depends.py + + :copyright: (C) 2014 by Openlabs Technologies & Consulting (P) Limited + :license: BSD, see LICENSE for more details. +""" +import sys +import os +DIR = os.path.abspath(os.path.normpath(os.path.join( + __file__, '..', '..', '..', '..', '..', 'trytond' +))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) +import unittest + +import trytond.tests.test_tryton +from trytond.tests.test_tryton import test_view, test_depends + + +class TestViewsDepends(unittest.TestCase): + ''' + Test views and depends + ''' + + def setUp(self): + """ + Set up data used in the tests. + this method is called before each test function execution. + """ + trytond.tests.test_tryton.install_module('party_merge') + + def test0005views(self): + ''' + Test views. + ''' + test_view('party_merge') + + def test0006depends(self): + ''' + Test depends. + ''' + test_depends() + + +def suite(): + """ + Define suite + """ + test_suite = trytond.tests.test_tryton.suite() + test_suite.addTests( + unittest.TestLoader().loadTestsFromTestCase(TestViewsDepends) + ) + return test_suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/tryton.cfg b/tryton.cfg new file mode 100644 index 0000000..0211c21 --- /dev/null +++ b/tryton.cfg @@ -0,0 +1,6 @@ +[tryton] +version=3.2.1.0 +depends: + party +xml: + party.xml diff --git a/view/party_form.xml b/view/party_form.xml new file mode 100644 index 0000000..aca933c --- /dev/null +++ b/view/party_form.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/view/party_merge_view_form.xml b/view/party_merge_view_form.xml new file mode 100644 index 0000000..0d265ed --- /dev/null +++ b/view/party_merge_view_form.xml @@ -0,0 +1,5 @@ + +
+