From 599a9b5a83acb92abd5b02336043a959b896d1ec Mon Sep 17 00:00:00 2001 From: Albert Cervera i Areny Date: Wed, 20 Jul 2016 11:08:12 +0200 Subject: [PATCH] Finish the module: do the polishing, tests and docs. --- __init__.py | 3 + doc/index.rst | 12 ++++ party.py | 138 +++++++++++++++++++++++++++++++++------- party.xml | 65 +++++++++++++++++++ tests/test_party_zip.py | 97 ++++++++++++++++++++++------ tryton.cfg | 1 + view/address_form.xml | 10 +++ 7 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 doc/index.rst create mode 100644 party.xml create mode 100644 view/address_form.xml diff --git a/__init__.py b/__init__.py index 16f373d..7407569 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,10 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.pool import Pool +from .party import * def register(): Pool.register( + Address, + CountryZip, module='party_zip', type_='model') diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..1c0e8cc --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,12 @@ +Party Zip Module +################ + +The Party Zip module adds the country zip field to addresses as well as menu +entries to Country Zip and Subdivision models. + +If installed, the module will make zip and city fields in addresses readonly +so they can only be filled in with the new country zip field. This allows all +addresses to have a zip/city that is previously created in the database. + +The module also takes care of updating all addresses if zip, city, subdivision +or country fields are changed in the zip record. diff --git a/party.py b/party.py index d44479e..bf12e6c 100644 --- a/party.py +++ b/party.py @@ -1,33 +1,123 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. -from trytond.model import ModelSQL, ModelView -from trytond.pool import PoolMeta +from trytond.model import fields +from trytond.pool import PoolMeta, Pool +from trytond.pyson import Eval, If, Bool -__all__ = ['Address'] -__metaclass__ = PoolMeta - - -# TODO: Create a view that adds 'country_zip' field and hides the following -# fields in the form view: -# -# - zip -# - city -# - country -# - subdivision -# -# (I think it is better to hide rather than replace so that other inheriting -# modules will still be compatible) -# -# For tree view I think it is better to keep existing fields +__all__ = ['Address', 'CountryZip'] class Address: __name__ = 'party.address' - country_zip = fields.Many2One('country.zip', 'Location') + __metaclass__ = PoolMeta + country_zip = fields.Many2One('country.zip', 'Location', + ondelete='RESTRICT', domain=[ + If(Bool(Eval('country')), ('country', '=', Eval('country', -1)), + ()), + If(Bool(Eval('subdivision')), + ('subdivision', '=', Eval('subdivision', -1)), ()), + ], depends=['country', 'subdivision']) -# TODO: Given that I think there are some issues if we try to redefine those -# fields as Function ones, we could consider storing their value in the -# database on write. The problem we need to consider is what happens if a field -# from country.zip is changed. Should we 'simply' propagate the new values to -# existing addresses? + @classmethod + def __setup__(cls): + super(Address, cls).__setup__() + cls.zip.readonly = True + cls.city.readonly = True + cls.country.states['readonly'] |= Bool(Eval('country_zip')) + cls.subdivision.states['readonly'] |= Bool(Eval('country_zip')) + @staticmethod + def update_zip_values(CountryZip, values): + values = values.copy() + if 'country_zip' in values: + if values['country_zip']: + country_zip, = CountryZip.search([ + ('id', '=', values['country_zip']), + ], limit=1) + values['zip'] = country_zip.zip + values['city'] = country_zip.city + values['country'] = country_zip.country.id + values['subdivision'] = (country_zip.subdivision.id if + country_zip.subdivision else None) + else: + values['zip'] = None + values['city'] = None + return values + + @classmethod + def create(cls, vlist): + CountryZip = Pool().get('country.zip') + new_vlist = [] + for values in vlist: + new_vlist.append(cls.update_zip_values(CountryZip, values)) + super(Address, cls).create(new_vlist) + + @classmethod + def write(cls, *args): + CountryZip = Pool().get('country.zip') + actions = iter(args) + new_args = [] + for addresses, values in zip(actions, actions): + new_args.append(addresses) + new_args.append(cls.update_zip_values(CountryZip, values)) + super(Address, cls).write(*new_args) + + @fields.depends('country_zip') + def on_change_country_zip(self): + if self.country_zip: + self.zip = self.country_zip.zip + self.city = self.country_zip.city + self.country = self.country_zip.country + self.subdivision = self.country_zip.subdivision + else: + self.zip = None + self.city = None + + +class CountryZip: + __name__ = 'country.zip' + __metaclass__ = PoolMeta + + def get_rec_name(self, name): + res = [] + if self.zip: + res.append(self.zip) + if self.city: + res.append(self.city) + res = [' '.join(res)] + if self.subdivision: + res.append(self.subdivision.rec_name) + res = [' - '.join(res)] + res.append('(%s)' % self.country.rec_name) + return ' '.join(res) + + @classmethod + def search_rec_name(cls, name, clause): + return ['OR', + [('zip',) + tuple(clause[1:])], + [('city',) + tuple(clause[1:])], + ] + + @classmethod + def write(cls, *args): + Address = Pool().get('party.address') + + super(CountryZip, cls).write(*args) + + actions = iter(args) + fields = set(['zip', 'city', 'country', 'subdivision']) + to_update = [] + for zips, values in zip(actions, actions): + intersec = set(values.keys()) & fields + if not intersec: + continue + addresses = Address.search([ + ('country_zip', 'in', [x.id for x in zips]), + ]) + to_update.append(addresses) + address_values = {} + for field in intersec: + address_values[field] = values[field] + to_update.append(address_values) + + Address.write(*to_update) diff --git a/party.xml b/party.xml new file mode 100644 index 0000000..00b19e6 --- /dev/null +++ b/party.xml @@ -0,0 +1,65 @@ + + + + party.address + + address_form + + + + Subdivisions + country.subdivision + + + + + + + + + + + + + + + Subdivisions + country.subdivision + + + + + + + + + + + + + + form_relate + country.country,-1 + + + + + Zips + country.zip + + + + + + + + + + + + + + diff --git a/tests/test_party_zip.py b/tests/test_party_zip.py index ad4345b..3e57098 100644 --- a/tests/test_party_zip.py +++ b/tests/test_party_zip.py @@ -1,33 +1,92 @@ # 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 test_view, test_depends -# TODO: Remove if no sceneario needed. -# from trytond.tests.test_tryton import doctest_setup, doctest_teardown +from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.pool import Pool -class TestCase(unittest.TestCase): - 'Test module' +class PartyZipTestCase(ModuleTestCase): + 'Test PartyZip module' + module = 'party_zip' - def setUp(self): - trytond.tests.test_tryton.install_module('party_zip') + @with_transaction() + def test_address(self): + 'Create address' + pool = Pool() + Party = pool.get('party.party') + Address = pool.get('party.address') + Country = pool.get('country.country') + Subdivision = pool.get('country.subdivision') + Zip = pool.get('country.zip') - def test0005views(self): - 'Test views' - test_view('party_zip') + country1, country2 = Country.create([{ + 'name': 'Country 1', + }, { + 'name': 'Country 2', + }]) + subdivision1, subdivision2 = Subdivision.create([{ + 'code': '1', + 'name': 'Subdivision 1', + 'type': 'area', + 'country': country1.id, + }, { + 'code': '2', + 'name': 'Subdivision 2', + 'type': 'area', + 'country': country2.id, + }]) + zip1, zip2 = Zip.create([{ + 'zip': 'zip1', + 'city': 'city1', + 'country': country1.id, + 'subdivision': subdivision1.id, + }, { + 'zip': 'zip2', + 'city': 'city2', + 'country': country2.id, + 'subdivision': subdivision2.id, + }]) + party1, = Party.create([{ + 'name': 'Party 1', + }]) + address, = Address.create([{ + 'party': party1.id, + 'street': 'St sample, 15', + 'city': 'City', + }]) + self.assertEqual(address.zip, None) + self.assertEqual(address.city, None) + Address.write([address, { + 'country_zip': zip1, + }]) + self.assertEqual(address.zip, 'zip1') + self.assertEqual(address.city, 'city1') + self.assertEqual(address.country.id, country1.id) + self.assertEqual(address.subdivision.id, subdivision1.id) - def test0006depends(self): - 'Test depends' - test_depends() + Address.write([address, { + 'country_zip': zip2, + }]) + self.assertEqual(address.zip, 'zip2') + self.assertEqual(address.city, 'city2') + self.assertEqual(address.country.id, country2.id) + self.assertEqual(address.subdivision.id, subdivision2.id) + + Zip.write([zip2, { + 'zip': 'ZIP 3', + 'city': 'CITY 3', + 'country': country1.id, + 'subdivision': subdivision1.id, + }]) + address, = Address.browse([address.id]) + self.assertEqual(address.zip, 'ZIP3') + self.assertEqual(address.city, 'CITY 3') + self.assertEqual(address.country.id, country1.id) + self.assertEqual(address.subdivision.id, subdivision1.id) def suite(): suite = trytond.tests.test_tryton.suite() - suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCase)) - # TODO: remove if no scenario needed. - #suite.addTests(doctest.DocFileSuite('scenario_party_zip.rst', - # setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8', - # optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(PartyZipTestCase)) return suite diff --git a/tryton.cfg b/tryton.cfg index 423880e..d23f4bc 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -4,3 +4,4 @@ depends: country party xml: + party.xml diff --git a/view/address_form.xml b/view/address_form.xml new file mode 100644 index 0000000..55fa34d --- /dev/null +++ b/view/address_form.xml @@ -0,0 +1,10 @@ + + + + + +