Browse Source

[MIG] base_location: Migration to 12.0

This module has now been refactored to be more consistent with what base_address_city offers to the location management.
Added dependency to contacts so that I could change the menu location for cities / zip management.

Now, every res.city record has a relation One2many to res.city.zip (old res.better.zip). This way, every zip has a realted city too.
Zips can be searched through city code, zip or city name (same as before).

Modified tests and deleted not needed tests.

Added sql contraints so that zips and cities are unique within it's country / state / city.
pull/638/head
Aitor Bouzas 6 years ago
committed by Pedro M. Baeza
parent
commit
f4f38c4c59
  1. 106
      base_location/README.rst
  2. 1
      base_location/__init__.py
  3. 24
      base_location/__openerp__.py
  4. 8
      base_location/demo/better_zip.xml
  5. 13
      base_location/demo/res_city_zip.xml
  6. 9
      base_location/models/__init__.py
  7. 108
      base_location/models/better_zip.py
  8. 19
      base_location/models/res_city.py
  9. 40
      base_location/models/res_city_zip.py
  10. 46
      base_location/models/res_company.py
  11. 60
      base_location/models/res_partner.py
  12. 11
      base_location/models/state.py
  13. 10
      base_location/readme/CONFIGURE.rst
  14. 9
      base_location/readme/CONTRIBUTORS.rst
  15. 1
      base_location/readme/CREDITS.rst
  16. 5
      base_location/readme/DESCRIPTION.rst
  17. 3
      base_location/readme/USAGE.rst
  18. 3
      base_location/security/ir.model.access.csv
  19. 463
      base_location/static/description/index.html
  20. 1
      base_location/tests/__init__.py
  21. 394
      base_location/tests/test_base_location.py
  22. 85
      base_location/views/better_zip_view.xml
  23. 63
      base_location/views/res_city_view.xml
  24. 56
      base_location/views/res_city_zip_view.xml
  25. 0
      base_location/views/res_company_view.xml
  26. 2
      base_location/views/res_country_view.xml
  27. 1
      base_location/views/res_partner_view.xml
  28. 26
      base_location/views/state_view.xml

106
base_location/README.rst

@ -1,33 +1,54 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
====================================
Location management (aka Better ZIP)
====================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpartner--contact-lightgray.png?logo=github
:target: https://github.com/OCA/partner-contact/tree/12.0/base_location
:alt: OCA/partner-contact
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/partner-contact-12-0/partner-contact-12-0-base_location
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/134/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module introduces a zip model that allows you to manage locations in a better way.
The zips will allow the users to complete automatically all address-related fields by just filling the zip.
=======================
Enhanced ZIP management
=======================
This module introduces a better zip/npa management system.
Also allows different search filters.
It enables zip, city, state and country auto-completion on partners and companies.
**Table of contents**
Also allows different search filters.
.. contents::
:local:
Configuration Configuration
============= =============
#. Activate the developer mode in *Settings*.
#. Go to *Settings / Technical / Locations Management / Locations*.
#. Create a new Location.
#. Go to *Contacts / Configuration / Localization / Cities*.
#. Create a new City.
#. Go to *Contacts / Configuration / Localization / Zips*.
#. Create a new Zip and relate it to the city (you can also create the Zip from the City).
or, with module 'Contacts Directory' installed: or, with module 'Contacts Directory' installed:
#. Go to *Contacts / Configuration / Localization / Countries*. #. Go to *Contacts / Configuration / Localization / Countries*.
#. Locate the desired country. #. Locate the desired country.
#. Press on the button 'Locations'.
or,
#. Go to *Contacts / Configuration / Localization / Fed. States*
#. Locate the desired state.
#. Enter the desired Locations.
#. Press on the button 'Cities' / 'Zips'.
Usage Usage
===== =====
@ -36,52 +57,59 @@ Usage
#. Fill the field *Location completion* #. Fill the field *Location completion*
#. Information about country, state, city and zip will be filled automatically #. Information about country, state, city and zip will be filled automatically
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/134/11.0
Bug Tracker Bug Tracker
=========== ===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/partner_contact/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Bugs are tracked on `GitHub Issues <https://github.com/OCA/partner-contact/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/partner-contact/issues/new?body=module:%20base_location%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits Credits
======= =======
Images
------
Authors
~~~~~~~
* Icon park: `Icon http://icon-park.com/icon/location-map-pin-orange3/`
* Camptocamp
* ACYSOS S.L.
* Alejandro Santana
* Tecnativa
* AdaptiveCity
Contributors Contributors
------------
~~~~~~~~~~~~
* Nicolas Bessi (Camptocamp) * Nicolas Bessi (Camptocamp)
* Ignacio Ibeas (Acysos S.L.) * Ignacio Ibeas (Acysos S.L.)
* Pedro M. Baeza <pedro.baeza@serviciosbaeza.com>
* Pedro M. Baeza <pedro.baeza@gmail.com>
* Alejandro Santana <alejandrosantana@anubia.es> * Alejandro Santana <alejandrosantana@anubia.es>
* Sandy Carter <sandy.carter@savoirfairelinux.com> * Sandy Carter <sandy.carter@savoirfairelinux.com>
* Yannick Vaucher <yannick.vaucher@camptocamp.com> * Yannick Vaucher <yannick.vaucher@camptocamp.com>
* Francesco Apruzzese <f.apruzzese@apuliasoftware.it> * Francesco Apruzzese <f.apruzzese@apuliasoftware.it>
* Dave Lasley <dave@laslabs.com> * Dave Lasley <dave@laslabs.com>
* Aitor Bouzas <aitor.bouzas@adaptivecity.com>
Other credits
~~~~~~~~~~~~~
* Icon park: `Icon http://icon-park.com/icon/location-map-pin-orange3/`
Maintainers
~~~~~~~~~~~
Maintainer
----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png .. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association :alt: Odoo Community Association
:target: https://odoo-community.org :target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and mission is to support the collaborative development of Odoo features and
promote its widespread use. promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.
This module is part of the `OCA/partner-contact <https://github.com/OCA/partner-contact/tree/12.0/base_location>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

1
base_location/__init__.py

@ -1,4 +1,3 @@
# Copyright 2016 Nicolas Bessi, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models from . import models

24
base_location/__openerp__.py

@ -4,26 +4,30 @@
{ {
'name': 'Location management (aka Better ZIP)', 'name': 'Location management (aka Better ZIP)',
'version': '11.0.1.0.1',
'version': '12.0.1.0.0',
'depends': [ 'depends': [
'base_address_city'
'base_address_city',
'contacts',
], ],
'author': "Camptocamp," 'author': "Camptocamp,"
"ACYSOS S.L.," "ACYSOS S.L.,"
"Alejandro Santana," "Alejandro Santana,"
"Tecnativa," "Tecnativa,"
"AdaptiveCity,"
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
'license': "AGPL-3", 'license': "AGPL-3",
'summary': '''Enhanced zip/npa management system''', 'summary': '''Enhanced zip/npa management system''',
'website': 'http://www.camptocamp.com',
'data': ['views/better_zip_view.xml',
'views/state_view.xml',
'views/res_country_view.xml',
'views/company_view.xml',
'views/partner_view.xml',
'security/ir.model.access.csv'],
'website': 'https://github.com/OCA/partner-contact',
'data': [
'security/ir.model.access.csv',
'views/res_city_zip_view.xml',
'views/res_city_view.xml',
'views/res_country_view.xml',
'views/res_company_view.xml',
'views/res_partner_view.xml',
],
'demo': [ 'demo': [
'demo/better_zip.xml',
'demo/res_city_zip.xml',
], ],
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,

8
base_location/demo/better_zip.xml

@ -1,8 +0,0 @@
<?xml version = "1.0" encoding="utf-8"?>
<odoo>
<record id="demo_brussels" model="res.better.zip">
<field name="name">1000</field>
<field name="city">Brussels</field>
<field name="country_id" ref="base.be"/>
</record>
</odoo>

13
base_location/demo/res_city_zip.xml

@ -0,0 +1,13 @@
<?xml version = "1.0" encoding="utf-8"?>
<odoo>
<record id="demo_brussels_city" model="res.city">
<field name="name">Brussels</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="demo_brussels_zip" model="res.city.zip">
<field name="name">1000</field>
<field name="city_id" ref="demo_brussels_city"/>
</record>
</odoo>

9
base_location/models/__init__.py

@ -1,7 +1,6 @@
# Copyright 2016 Nicolas Bessi, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import better_zip
from . import partner
from . import state
from . import company
from . import res_city_zip
from . import res_partner
from . import res_company
from . import res_city

108
base_location/models/better_zip.py

@ -1,108 +0,0 @@
# Copyright 2016 Nicolas Bessi, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class BetterZip(models.Model):
'''City/locations completion object'''
_name = "res.better.zip"
_description = __doc__
_order = "name asc"
name = fields.Char('ZIP')
code = fields.Char(
'City Code',
size=64,
help="The official code for the city"
)
city = fields.Char('City', required=True)
city_id = fields.Many2one(
'res.city',
'City',
)
state_id = fields.Many2one(
'res.country.state',
'State',
)
country_id = fields.Many2one('res.country', 'Country')
enforce_cities = fields.Boolean(
related='country_id.enforce_cities',
readonly=True,
)
latitude = fields.Float()
longitude = fields.Float()
@api.multi
@api.depends('name', 'city', 'state_id', 'country_id')
def name_get(self):
result = []
for rec in self:
name = []
if rec.name:
name.append('%(name)s' % {'name': rec.name})
name.append('%(name)s' % {'name': rec.city})
if rec.state_id:
name.append('%(name)s' % {'name': rec.state_id.name})
if rec.country_id:
name.append('%(name)s' % {'name': rec.country_id.name})
result.append((rec.id, ", ".join(name)))
return result
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
args = list(args or [])
args += ['|', ('city', operator, name),
'|', ('name', operator, name), ('code', operator, name)]
recs = self.search(args, limit=limit)
return recs.name_get()
@api.onchange('country_id')
def _onchange_country_id(self):
if self.state_id.country_id != self.country_id:
self.state_id = False
if self.city_id.country_id != self.country_id:
self.city_id = False
if self.country_id:
domain = [('country_id', '=', self.country_id.id)]
else:
domain = []
return {
'domain': {
'state_id': domain,
'city_id': domain,
}
}
@api.onchange('city_id')
def _onchange_city_id(self):
if self.city_id:
self.city = self.city_id.name
self.country_id = self.city_id.country_id
self.state_id = self.city_id.state_id
@api.onchange('state_id')
def _onchange_state_id(self):
if self.state_id:
self.country_id = self.state_id.country_id
@api.constrains('state_id', 'country_id', 'city_id')
def constrains_country(self):
for rec in self:
if rec.state_id and rec.state_id.country_id != \
rec.country_id:
raise ValidationError(_(
"The country of the state differs from the country in "
"location %s") % rec.name)
if rec.city_id and rec.city_id.country_id \
!= rec.country_id:
raise ValidationError(_(
"The country of the city differs from the country in "
"location %s") % rec.name)
if rec.city_id and rec.city_id.state_id \
!= rec.state_id:
raise ValidationError(_(
"The state of the city differs from the state in "
"location %s") % rec.name)

19
base_location/models/res_city.py

@ -0,0 +1,19 @@
# Copyright 2018 Aitor Bouzas <aitor.bouzas@adaptivecity.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class City(models.Model):
_inherit = 'res.city'
zip_ids = fields.One2many('res.city.zip', 'city_id',
string="Zips in this city")
_sql_constraints = [
('name_state_country_uniq',
'UNIQUE(name, state_id, country_id)',
'You already have a city with that name in the same state.'
'The city must have a unique name within '
'it\'s state and it\'s country'),
]

40
base_location/models/res_city_zip.py

@ -0,0 +1,40 @@
# Copyright 2016 Nicolas Bessi, Camptocamp SA
# Copyright 2018 Aitor Bouzas <aitor.bouzas@adaptivecity.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class ResCityZip(models.Model):
"""City/locations completion object"""
_name = "res.city.zip"
_description = __doc__
_order = "name asc"
_rec_name = "display_name"
name = fields.Char('ZIP', required=True)
city_id = fields.Many2one(
'res.city',
'City',
required=True,
)
display_name = fields.Char(compute='_compute_new_display_name',
store=True, index=True)
_sql_constraints = [
('name_city_uniq', 'UNIQUE(name, city_id)',
'You already have a zip with that code in the same city. '
'The zip code must be unique within it\'s city'),
]
@api.multi
@api.depends('name', 'city_id')
def _compute_new_display_name(self):
for rec in self:
name = [rec.name, rec.city_id.name]
if rec.city_id.state_id:
name.append(rec.city_id.state_id.name)
if rec.city_id.country_id:
name.append(rec.city_id.country_id.name)
rec.display_name = ", ".join(name)

46
base_location/models/company.py → base_location/models/res_company.py

@ -8,38 +8,39 @@ from odoo import models, fields, api
class ResCompany(models.Model): class ResCompany(models.Model):
_inherit = 'res.company' _inherit = 'res.company'
# In order to keep the same logic used in Odoo, fields must be computed
# and inversed, not related. This way we can ensure that it works
# correctly on changes and that inconsistencies cannot happen.
# When you make the fields related, the constrains added in res.partner
# will fail, because when you change the city_id in the company, you are
# effectively changing it in the partner. The constrains on the partner
# are evaluated before the inverse methods update the other fields (city,
# etc..) so we need them to ensure consistency.
# As a conclusion, address fields are very related to each other.
# Either you make them all related to the partner in company, or you
# don't for all of them. Mixing both approaches produces inconsistencies.
city_id = fields.Many2one( city_id = fields.Many2one(
'res.city', 'res.city',
compute='_compute_address', compute='_compute_address',
inverse='_inverse_city_id', inverse='_inverse_city_id',
string="City"
string="City ID"
) )
zip_id = fields.Many2one( zip_id = fields.Many2one(
'res.better.zip',
'res.city.zip',
string='ZIP Location', string='ZIP Location',
compute='_compute_address', compute='_compute_address',
inverse='_inverse_zip_id', inverse='_inverse_zip_id',
oldname="better_zip_id", oldname="better_zip_id",
help='Use the city name or the zip code to search the location', help='Use the city name or the zip code to search the location',
) )
# In order to keep the same logic used in odoo, fields must be computed
# and inversed, not related. This way, we can ensure that it works
# correctly on changes and inconsistencies cannot happen.
# When you make the fields related, the constrains added in res.partner
# will fail. because when you change the city_id in the company, you are
# effectively changing it in the partner. The constrains on the partner
# are evaluated before the inverse methods update the other fields (city,
# etc..). And we need constrains in the partner to ensure consistency.
# So, as a conclusion, address fields are very related to each other.
# Either you make them all related to the partner in company, or you
# don't for all of them. But mixing methods produces inconsistencies.
country_enforce_cities = fields.Boolean( country_enforce_cities = fields.Boolean(
related='country_id.enforce_cities' related='country_id.enforce_cities'
) )
def _get_company_address_fields(self, partner): def _get_company_address_fields(self, partner):
res = super(ResCompany, self)._get_company_address_fields(partner)
res = super()._get_company_address_fields(partner)
res['city_id'] = partner.city_id res['city_id'] = partner.city_id
res['zip_id'] = partner.zip_id res['zip_id'] = partner.zip_id
return res return res
@ -55,16 +56,15 @@ class ResCompany(models.Model):
@api.onchange('zip_id') @api.onchange('zip_id')
def _onchange_zip_id(self): def _onchange_zip_id(self):
if self.zip_id: if self.zip_id:
self.zip = self.zip_id.name
self.city_id = self.zip_id.city_id
self.city = self.zip_id.city
self.country_id = self.zip_id.country_id
if self.country_id.enforce_cities:
self.state_id = self.city_id.state_id
else:
self.state_id = self.zip_id.state_id
self.update({
'zip': self.zip_id.name,
'city_id': self.zip_id.city_id,
'city': self.zip_id.city_id.name,
'country_id': self.zip_id.city_id.country_id,
'state_id': self.zip_id.city_id.state_id,
})
@api.onchange('state_id') @api.onchange('state_id')
def onchange_state_id(self):
def _onchange_state_id(self):
if self.state_id.country_id: if self.state_id.country_id:
self.country_id = self.state_id.country_id.id self.country_id = self.state_id.country_id.id

60
base_location/models/partner.py → base_location/models/res_partner.py

@ -8,16 +8,19 @@ from odoo.exceptions import ValidationError
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
zip_id = fields.Many2one('res.better.zip', 'ZIP Location')
zip_id = fields.Many2one('res.city.zip', 'ZIP Location')
@api.onchange('city_id') @api.onchange('city_id')
def _onchange_city_id(self): def _onchange_city_id(self):
if not self.zip_id: if not self.zip_id:
super(ResPartner, self)._onchange_city_id()
super()._onchange_city_id()
if self.zip_id and self.city_id != self.zip_id.city_id: if self.zip_id and self.city_id != self.zip_id.city_id:
self.zip_id = False
self.zip = False
self.city = False
self.update({
'zip_id': False,
'zip': False,
'city': False,
})
if self.city_id: if self.city_id:
return { return {
'domain': { 'domain': {
@ -26,38 +29,37 @@ class ResPartner(models.Model):
} }
return {'domain': {'zip_id': []}} return {'domain': {'zip_id': []}}
@api.onchange('state_id')
def _onchange_state_id(self):
if self.zip_id and self.state_id != self.zip_id.state_id:
self.zip_id = False
self.zip = False
self.city = False
@api.onchange('country_id') @api.onchange('country_id')
def _onchange_country_id(self): def _onchange_country_id(self):
res = super(ResPartner, self)._onchange_country_id()
if self.zip_id and self.zip_id.country_id != self.country_id:
res = super()._onchange_country_id()
if self.zip_id and self.zip_id.city_id.country_id != self.country_id:
self.zip_id = False self.zip_id = False
return res return res
@api.onchange('zip_id') @api.onchange('zip_id')
def _onchange_zip_id(self): def _onchange_zip_id(self):
if self.zip_id: if self.zip_id:
self.country_id = self.zip_id.country_id
if self.country_id.enforce_cities:
self.city_id = self.zip_id.city_id
self.zip = self.zip_id.name
self.state_id = self.zip_id.state_id
self.city = self.zip_id.city
vals = {
'city_id': self.zip_id.city_id,
'zip': self.zip_id.name,
'city': self.zip_id.city_id.name,
}
if self.zip_id.city_id.country_id:
vals.update({'country_id': self.zip_id.city_id.country_id})
if self.zip_id.city_id.state_id:
vals.update({'state_id': self.zip_id.city_id.state_id})
self.update(vals)
@api.constrains('zip_id', 'country_id', 'city_id', 'state_id') @api.constrains('zip_id', 'country_id', 'city_id', 'state_id')
def _check_zip(self): def _check_zip(self):
for rec in self.filtered('zip_id'):
if rec.zip_id.state_id != rec.state_id:
for rec in self:
if not rec.zip_id:
continue
if rec.zip_id.city_id.state_id != rec.state_id:
raise ValidationError(_( raise ValidationError(_(
"The state of the partner %s differs from that in " "The state of the partner %s differs from that in "
"location %s") % (rec.name, rec.zip_id.name)) "location %s") % (rec.name, rec.zip_id.name))
if rec.zip_id.country_id != rec.country_id:
if rec.zip_id.city_id.country_id != rec.country_id:
raise ValidationError(_( raise ValidationError(_(
"The country of the partner %s differs from that in " "The country of the partner %s differs from that in "
"location %s") % (rec.name, rec.zip_id.name)) "location %s") % (rec.name, rec.zip_id.name))
@ -67,6 +69,14 @@ class ResPartner(models.Model):
"location %s") % (rec.name, rec.zip_id.name)) "location %s") % (rec.name, rec.zip_id.name))
@api.onchange('state_id') @api.onchange('state_id')
def onchange_state_id(self):
def _onchange_state_id(self):
vals = {}
if self.state_id.country_id: if self.state_id.country_id:
self.country_id = self.state_id.country_id.id
vals.update({'country_id': self.state_id.country_id})
if self.zip_id and self.state_id != self.zip_id.city_id.state_id:
vals.update({
'zip_id': False,
'zip': False,
'city': False,
})
self.update(vals)

11
base_location/models/state.py

@ -1,11 +0,0 @@
# Copyright 2016 Nicolas Bessi, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class ResCountryState(models.Model):
_inherit = 'res.country.state'
better_zip_ids = fields.One2many('res.better.zip', 'state_id', 'Cities')

10
base_location/readme/CONFIGURE.rst

@ -0,0 +1,10 @@
#. Go to *Contacts / Configuration / Localization / Cities*.
#. Create a new City.
#. Go to *Contacts / Configuration / Localization / Zips*.
#. Create a new Zip and relate it to the city (you can also create the Zip from the City).
or, with module 'Contacts Directory' installed:
#. Go to *Contacts / Configuration / Localization / Countries*.
#. Locate the desired country.
#. Press on the button 'Cities' / 'Zips'.

9
base_location/readme/CONTRIBUTORS.rst

@ -0,0 +1,9 @@
* Nicolas Bessi (Camptocamp)
* Ignacio Ibeas (Acysos S.L.)
* Pedro M. Baeza <pedro.baeza@gmail.com>
* Alejandro Santana <alejandrosantana@anubia.es>
* Sandy Carter <sandy.carter@savoirfairelinux.com>
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
* Francesco Apruzzese <f.apruzzese@apuliasoftware.it>
* Dave Lasley <dave@laslabs.com>
* Aitor Bouzas <aitor.bouzas@adaptivecity.com>

1
base_location/readme/CREDITS.rst

@ -0,0 +1 @@
* Icon park: `Icon http://icon-park.com/icon/location-map-pin-orange3/`

5
base_location/readme/DESCRIPTION.rst

@ -0,0 +1,5 @@
This module introduces a zip model that allows you to manage locations in a better way.
The zips will allow the users to complete automatically all address-related fields by just filling the zip.
Also allows different search filters.

3
base_location/readme/USAGE.rst

@ -0,0 +1,3 @@
#. Access a partner record
#. Fill the field *Location completion*
#. Information about country, state, city and zip will be filled automatically

3
base_location/security/ir.model.access.csv

@ -1,3 +1,2 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"ir_model_access_betterzip0","res_better_zip group_user_all","model_res_better_zip",base.group_user,1,0,0,0
"ir_model_access_betterzip1","res_better_zip group_user","model_res_better_zip","base.group_partner_manager",1,1,1,1
"ir_model_access_cityzip0","res_city_zip group_user","model_res_city_zip","base.group_partner_manager",1,1,1,1

463
base_location/static/description/index.html

@ -0,0 +1,463 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
<title>Location management (aka Better ZIP)</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="location-management-aka-better-zip">
<h1 class="title">Location management (aka Better ZIP)</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/partner-contact/tree/12.0/base_location"><img alt="OCA/partner-contact" src="https://img.shields.io/badge/github-OCA%2Fpartner--contact-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/partner-contact-12-0/partner-contact-12-0-base_location"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/134/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module introduces a zip model that allows you to manage locations in a better way.</p>
<p>The zips will allow the users to complete automatically all address-related fields by just filling the zip.</p>
<p>Also allows different search filters.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="id7">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<ol class="arabic simple">
<li>Go to <em>Contacts / Configuration / Localization / Cities</em>.</li>
<li>Create a new City.</li>
<li>Go to <em>Contacts / Configuration / Localization / Zips</em>.</li>
<li>Create a new Zip and relate it to the city (you can also create the Zip from the City).</li>
</ol>
<p>or, with module ‘Contacts Directory’ installed:
#. Go to <em>Contacts / Configuration / Localization / Countries</em>.
#. Locate the desired country.
#. Press on the button ‘Cities’ / ‘Zips’.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<ol class="arabic simple">
<li>Access a partner record</li>
<li>Fill the field <em>Location completion</em></li>
<li>Information about country, state, city and zip will be filled automatically</li>
</ol>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/partner-contact/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/partner-contact/issues/new?body=module:%20base_location%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
<li>ACYSOS S.L.</li>
<li>Alejandro Santana</li>
<li>Tecnativa</li>
<li>AdaptiveCity</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Nicolas Bessi (Camptocamp)</li>
<li>Ignacio Ibeas (Acysos S.L.)</li>
<li>Pedro M. Baeza &lt;<a class="reference external" href="mailto:pedro.baeza&#64;gmail.com">pedro.baeza&#64;gmail.com</a>&gt;</li>
<li>Alejandro Santana &lt;<a class="reference external" href="mailto:alejandrosantana&#64;anubia.es">alejandrosantana&#64;anubia.es</a>&gt;</li>
<li>Sandy Carter &lt;<a class="reference external" href="mailto:sandy.carter&#64;savoirfairelinux.com">sandy.carter&#64;savoirfairelinux.com</a>&gt;</li>
<li>Yannick Vaucher &lt;<a class="reference external" href="mailto:yannick.vaucher&#64;camptocamp.com">yannick.vaucher&#64;camptocamp.com</a>&gt;</li>
<li>Francesco Apruzzese &lt;<a class="reference external" href="mailto:f.apruzzese&#64;apuliasoftware.it">f.apruzzese&#64;apuliasoftware.it</a>&gt;</li>
<li>Dave Lasley &lt;<a class="reference external" href="mailto:dave&#64;laslabs.com">dave&#64;laslabs.com</a>&gt;</li>
<li>Aitor Bouzas &lt;<a class="reference external" href="mailto:aitor.bouzas&#64;adaptivecity.com">aitor.bouzas&#64;adaptivecity.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#id7">Other credits</a></h2>
<ul class="simple">
<li>Icon park: <cite>Icon http://icon-park.com/icon/location-map-pin-orange3/</cite></li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/partner-contact/tree/12.0/base_location">OCA/partner-contact</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

1
base_location/tests/__init__.py

@ -1,4 +1,3 @@
# Copyright 2015 Yannick Vaucher, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_base_location from . import test_base_location

394
base_location/tests/test_base_location.py

@ -1,94 +1,119 @@
# Copyright 2015 Yannick Vaucher, Camptocamp SA # Copyright 2015 Yannick Vaucher, Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tests import tagged, common
from odoo.tools.misc import mute_logger
import psycopg2
@tagged('post_install', '-at_install')
class TestBaseLocation(common.SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
country_obj = cls.env['res.country.state']
city_obj = cls.env['res.city']
zip_obj = cls.env['res.city.zip']
cls.partner_obj = cls.env['res.partner']
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.state_vd = country_obj.create({
'name': 'Vaud',
'code': 'VD',
'country_id': cls.env.ref('base.ch').id,
})
cls.env.ref('base.es').write({
'enforce_cities': True
})
cls.company = cls.env.ref('base.main_company')
class TestBaseLocation(TransactionCase):
def test_onchange_better_zip_state_id(self):
""" Test onchange on res.better.zip """
usa_MA = self.env.ref('base.state_us_34')
better_zip1 = self.env['res.better.zip'].new(self.values_better_zip1())
better_zip1.state_id = usa_MA
better_zip1._onchange_state_id()
self.assertEqual(better_zip1.country_id, usa_MA.country_id)
def test_onchange_better_zip_city_id(self):
better_zip2 = self.env['res.better.zip'].new(self.values_better_zip2())
better_zip2.city_id = self.city_madrid
better_zip2._onchange_city_id()
self.assertEqual(better_zip2.city, self.city_madrid.name)
def test_onchange_better_zip_country_id(self):
better_zip1 = self.env['res.better.zip'].new(self.values_better_zip1())
better_zip1.country_id = self.env.ref('base.es')
better_zip1._onchange_country_id()
self.assertFalse(better_zip1.state_id)
def test_onchange_better_zip_none(self):
better_zip1 = self.env['res.better.zip'].new(self.values_better_zip1())
better_zip1.country_id = False
better_zip1._onchange_country_id()
self.assertFalse(better_zip1.state_id)
cls.state_bcn = country_obj.create({
'name': 'Barcelona',
'code': '08',
'country_id': cls.env.ref('base.es').id,
})
cls.state_madrid = country_obj.create({
'name': 'Madrid',
'code': '28',
'country_id': cls.env.ref('base.es').id,
})
cls.city_bcn = city_obj.create({
'name': 'Barcelona',
'state_id': cls.state_bcn.id,
'country_id': cls.env.ref('base.es').id,
})
cls.city_madrid = city_obj.create({
'name': 'Madrid',
'state_id': cls.state_madrid.id,
'country_id': cls.env.ref('base.es').id,
})
cls.city_lausanne = city_obj.create({
'name': 'Lausanne',
'state_id': cls.state_vd.id,
'country_id': cls.env.ref('base.ch').id,
})
cls.lausanne = zip_obj.create({
'name': '666',
'city_id': cls.city_lausanne.id,
})
cls.barcelona = zip_obj.create({
'name': '444',
'city_id': cls.city_bcn.id,
})
def test_onchange_partner_city_completion(self): def test_onchange_partner_city_completion(self):
partner1 = self.env['res.partner'].new({
"""Test that partner data is filled accodingly"""
partner1 = self.partner_obj.new({
'name': 'Camptocamp', 'name': 'Camptocamp',
}) })
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
better_zip2.country_id.enforce_cities = True
partner1.zip_id = better_zip2
self.barcelona.city_id.country_id.enforce_cities = True
partner1.zip_id = self.barcelona
partner1._onchange_zip_id() partner1._onchange_zip_id()
self.assertEqual(partner1.zip, better_zip2.name)
self.assertEqual(partner1.city, better_zip2.city)
self.assertEqual(partner1.state_id, better_zip2.state_id)
self.assertEqual(partner1.country_id, better_zip2.country_id)
self.assertEqual(partner1.zip, self.barcelona.name)
self.assertEqual(partner1.city, self.barcelona.city_id.name)
self.assertEqual(partner1.state_id, self.barcelona.city_id.state_id)
self.assertEqual(partner1.country_id,
self.barcelona.city_id.country_id)
def test_onchange_company_city_completion(self): def test_onchange_company_city_completion(self):
"""Test that company data is filled accodingly"""
company = self.env['res.company'].new({'name': 'Test'}) company = self.env['res.company'].new({'name': 'Test'})
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
company.zip_id = better_zip1
company.zip_id = self.lausanne
company._onchange_zip_id() company._onchange_zip_id()
self.assertEqual(company.zip, better_zip1.name)
self.assertEqual(company.city, better_zip1.city)
self.assertEqual(company.state_id, better_zip1.state_id)
self.assertEqual(company.country_id, better_zip1.country_id)
self.assertEqual(company.zip, self.lausanne.name)
self.assertEqual(company.city, self.lausanne.city_id.name)
self.assertEqual(company.state_id, self.lausanne.city_id.state_id)
self.assertEqual(company.country_id, self.lausanne.city_id.country_id)
def test_company_address_fields(self): def test_company_address_fields(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1()
)
"""Test if the partner address fields changes when
changing the ones from the company"""
company = self.env['res.company'].create({ company = self.env['res.company'].create({
'name': 'Test', 'name': 'Test',
}) })
self.assertTrue(company.partner_id) self.assertTrue(company.partner_id)
company.partner_id.write({ company.partner_id.write({
'zip_id': better_zip1.id,
'state_id': better_zip1.state_id.id,
'country_id': better_zip1.country_id.id,
'city_id': better_zip1.city_id.id,
'city': better_zip1.city,
'zip': better_zip1.name,
'zip_id': self.lausanne.id,
'state_id': self.lausanne.city_id.state_id.id,
'country_id': self.lausanne.city_id.country_id.id,
'city_id': self.lausanne.city_id.id,
'city': self.lausanne.city_id.name,
'zip': self.lausanne.name,
}) })
company._compute_address() company._compute_address()
self.assertEqual(company.zip_id, company.partner_id.zip_id) self.assertEqual(company.zip_id, company.partner_id.zip_id)
self.assertEqual(company.city_id, company.partner_id.city_id) self.assertEqual(company.city_id, company.partner_id.city_id)
def test_company_address_fields_inverse(self): def test_company_address_fields_inverse(self):
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2()
)
"""Test inverse fields from res.company"""
company = self.env['res.company'].new({ company = self.env['res.company'].new({
'name': 'Test', 'name': 'Test',
'partner_id': self.env['res.partner'].new({}).id
'partner_id': self.partner_obj.new({}).id
# Partner must be initiated in order to be filled # Partner must be initiated in order to be filled
}) })
company.update({ company.update({
'zip_id': better_zip2.id,
'zip_id': self.barcelona.id,
}) })
company._inverse_city_id() company._inverse_city_id()
company._inverse_zip_id() company._inverse_zip_id()
@ -96,112 +121,76 @@ class TestBaseLocation(TransactionCase):
self.assertEqual(company.city_id, company.partner_id.city_id) self.assertEqual(company.city_id, company.partner_id.city_id)
def test_onchange_company_city_id_completion(self): def test_onchange_company_city_id_completion(self):
"""Test city auto-completion when changing zip in a company"""
company = self.env['res.company'].new({'name': 'Test'}) company = self.env['res.company'].new({'name': 'Test'})
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
company.zip_id = better_zip2
company.zip_id = self.barcelona
company._onchange_zip_id() company._onchange_zip_id()
self.assertEqual(company.city_id, better_zip2.city_id)
def test_constrains_better_zip_01(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
better_zip1.city_id = self.city_lausanne
with self.assertRaises(ValidationError):
better_zip2.city_id = better_zip1.city_id
def test_constrains_better_zip_02(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
with self.assertRaises(ValidationError):
better_zip2.country_id = better_zip1.country_id
def test_constrains_better_zip_03(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
with self.assertRaises(ValidationError):
better_zip2.state_id = better_zip1.state_id
def test_constrains_better_zip_04(self):
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
with self.assertRaises(ValidationError):
better_zip2.city_id = self.city_madrid
self.assertEqual(company.city_id, self.barcelona.city_id)
def test_constrains_partner_01(self): def test_constrains_partner_01(self):
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
"""Test partner 1 constraints"""
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.env['res.partner'].create({
self.partner_obj.create({
'name': 'P1', 'name': 'P1',
'zip_id': better_zip2.id,
'zip_id': self.barcelona.id,
}) })
def test_constrains_partner_02(self):
better_zip2 = self.env['res.better.zip'].create(
self.values_better_zip2())
partner = self.env['res.partner'].create({
def test_constrains_partner_country(self):
"""Test partner country constraints"""
partner = self.partner_obj.create({
'name': 'P1', 'name': 'P1',
'zip_id': better_zip2.id,
'country_id': better_zip2.country_id.id,
'state_id': better_zip2.state_id.id,
'city_id': better_zip2.city_id.id,
'zip_id': self.barcelona.id,
'country_id': self.barcelona.city_id.country_id.id,
'state_id': self.barcelona.city_id.state_id.id,
'city_id': self.barcelona.city_id.id,
}) })
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
partner.country_id = self.ref('base.ch') partner.country_id = self.ref('base.ch')
with self.assertRaises(ValidationError):
partner.state_id = self.state_vd.id,
def test_constrains_partner_state(self):
"""Test partner state constraints"""
partner = self.partner_obj.create({
'name': 'P1',
'zip_id': self.barcelona.id,
'country_id': self.barcelona.city_id.country_id.id,
'state_id': self.barcelona.city_id.state_id.id,
'city_id': self.barcelona.city_id.id,
})
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
partner.city_id = self.city_lausanne
partner.state_id = self.state_vd.id
def values_better_zip1(self):
return {
'name': 1000,
'city': 'Lausanne',
'state_id': self.state_vd.id,
'country_id': self.ref('base.ch'),
}
def test_constrains_partner_city(self):
"""Test partner city constraints"""
partner = self.partner_obj.create({
'name': 'P1',
'zip_id': self.barcelona.id,
'country_id': self.barcelona.city_id.country_id.id,
'state_id': self.barcelona.city_id.state_id.id,
'city_id': self.barcelona.city_id.id,
})
def values_better_zip2(self):
return {
'city_id': self.city_bcn.id,
'city': self.city_bcn.name,
'state_id': self.state_bcn.id,
'country_id': self.ref('base.es'),
}
with self.assertRaises(ValidationError):
partner.city_id = self.city_lausanne
def test_partner_onchange_country(self): def test_partner_onchange_country(self):
country_es = self.browse_ref('base.es')
"""Test partner onchange country_id"""
country_es = self.env.ref('base.es')
country_es.enforce_cities = True country_es.enforce_cities = True
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
partner = self.env['res.partner'].new({
partner = self.partner_obj.new({
'name': 'TEST', 'name': 'TEST',
'zip_id': better_zip1.id
'zip_id': self.lausanne.id
}) })
partner.country_id = country_es partner.country_id = country_es
partner._onchange_country_id() partner._onchange_country_id()
self.assertFalse(partner.zip_id) self.assertFalse(partner.zip_id)
def test_partner_onchange_city(self): def test_partner_onchange_city(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
partner = self.env['res.partner'].new({
"""Test partner onchange city_id"""
partner = self.partner_obj.new({
'name': 'TEST', 'name': 'TEST',
'zip_id': better_zip1.id
'zip_id': self.lausanne.id
}) })
self.city_bcn.country_id.enforce_cities = False self.city_bcn.country_id.enforce_cities = False
partner.city_id = self.city_bcn partner.city_id = self.city_bcn
@ -212,119 +201,78 @@ class TestBaseLocation(TransactionCase):
self.assertFalse(res['domain']['zip_id']) self.assertFalse(res['domain']['zip_id'])
def test_partner_onchange_state(self): def test_partner_onchange_state(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
partner = self.env['res.partner'].new({
"""Test partner onchange state_id"""
partner = self.partner_obj.new({
'name': 'TEST', 'name': 'TEST',
'zip_id': better_zip1.id
'zip_id': self.lausanne.id
}) })
partner.state_id = self.state_bcn partner.state_id = self.state_bcn
partner._onchange_state_id() partner._onchange_state_id()
self.assertFalse(partner.zip_id) self.assertFalse(partner.zip_id)
self.assertEqual(partner.country_id, partner.state_id.country_id)
def test_company_onchange_state(self):
"""Test company onchange state_id"""
self.company.state_id = self.state_bcn
self.company._onchange_state_id()
self.assertEqual(self.company.country_id,
self.company.state_id.country_id)
def test_display_name(self): def test_display_name(self):
better_zip1 = self.env['res.better.zip'].create(
self.values_better_zip1())
"""Test if the display_name is stored and computed properly"""
self.assertEqual( self.assertEqual(
better_zip1.display_name, '1000, Lausanne, Vaud, '+self.browse_ref(
self.lausanne.display_name,
'666, Lausanne, Vaud, ' + self.browse_ref(
'base.ch' 'base.ch'
).name ).name
) )
def test_name_search___can_find_using_city_name_or_zip_or_code(self):
barcelona_data = {
'city_id': self.city_bcn.id,
'city': self.city_bcn.name,
'name': '444',
'code': 'BA',
'state_id': self.state_bcn.id,
'country_id': self.ref('base.es'),
}
def test_name_search(self):
"""Test that zips can be searched through both the name of the
city or the zip code"""
madrid_data = { madrid_data = {
'city_id': self.city_madrid.id, 'city_id': self.city_madrid.id,
'city': self.city_madrid.name,
'name': '555', 'name': '555',
'code': 'MD',
'state_id': self.state_madrid.id,
'country_id': self.ref('base.es'),
}
lausanne_data = {
'city_id': self.city_lausanne.id,
'city': self.city_lausanne.name,
'name': '666',
'code': 'LA',
'state_id': self.state_vd.id,
'country_id': self.ref('base.ch'),
} }
barcelona = self.env['res.better.zip'].create(barcelona_data)
madrid = self.env['res.better.zip'].create(madrid_data)
lausanne = self.env['res.better.zip'].create(lausanne_data)
madrid = self.env['res.city.zip'].create(madrid_data)
found_recs = self.env['res.better.zip'].name_search(name='444')
found_recs = self.env['res.city.zip'].name_search(name='444')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], barcelona.id)
found_recs = self.env['res.better.zip'].name_search(name='Barcelona')
self.assertEqual(found_recs[0][0], self.barcelona.id)
found_recs = self.env['res.city.zip'].name_search(name='Barcelona')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], barcelona.id)
found_recs = self.env['res.better.zip'].name_search(name='BA')
self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], barcelona.id)
self.assertEqual(found_recs[0][0], self.barcelona.id)
found_recs = self.env['res.better.zip'].name_search(name='555')
self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], madrid.id)
found_recs = self.env['res.better.zip'].name_search(name='Madrid')
found_recs = self.env['res.city.zip'].name_search(name='555')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], madrid.id) self.assertEqual(found_recs[0][0], madrid.id)
found_recs = self.env['res.better.zip'].name_search(name='MD')
found_recs = self.env['res.city.zip'].name_search(name='Madrid')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], madrid.id) self.assertEqual(found_recs[0][0], madrid.id)
found_recs = self.env['res.better.zip'].name_search(name='666')
found_recs = self.env['res.city.zip'].name_search(name='666')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], lausanne.id)
found_recs = self.env['res.better.zip'].name_search(name='Lausanne')
self.assertEqual(found_recs[0][0], self.lausanne.id)
found_recs = self.env['res.city.zip'].name_search(name='Lausanne')
self.assertEqual(len(found_recs), 1) self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], lausanne.id)
found_recs = self.env['res.better.zip'].name_search(name='LA')
self.assertEqual(len(found_recs), 1)
self.assertEqual(found_recs[0][0], lausanne.id)
def setUp(self):
super(TestBaseLocation, self).setUp()
self.state_vd = self.env['res.country.state'].create({
'name': 'Vaud',
'code': 'VD',
'country_id': self.ref('base.ch'),
})
self.env['res.country'].browse(self.ref('base.es')).write({
'enforce_cities': True
})
self.company = self.env.ref('base.main_company')
self.assertEqual(found_recs[0][0], self.lausanne.id)
def test_zip_ql_constraints(self):
"""Test UNIQUE name within it's area for zips"""
with self.assertRaises(
psycopg2.IntegrityError), mute_logger('odoo.sql_db'):
self.env['res.city.zip'].create({
'name': '666',
'city_id': self.city_lausanne.id,
})
self.state_bcn = self.env['res.country.state'].create({
'name': 'Barcelona',
'code': '08',
'country_id': self.ref('base.es'),
})
self.state_madrid = self.env['res.country.state'].create({
'name': 'Madrid',
'code': '28',
'country_id': self.ref('base.es'),
})
self.city_bcn = self.env['res.city'].create({
'name': 'Barcelona',
'state_id': self.state_bcn.id,
'country_id': self.ref('base.es'),
})
self.city_madrid = self.env['res.city'].create({
'name': 'Madrid',
'state_id': self.state_madrid.id,
'country_id': self.ref('base.es'),
})
self.city_lausanne = self.env['res.city'].create({
'name': 'Lausanne',
'state_id': self.state_vd.id,
'country_id': self.ref('base.ch'),
})
def test_city_sql_contraint(self):
"""Test UNIQUE name within it's area for cities"""
with self.assertRaises(
psycopg2.IntegrityError), mute_logger('odoo.sql_db'):
self.env['res.city'].create({
'name': 'Barcelona',
'state_id': self.state_bcn.id,
'country_id': self.ref('base.es'),
})

85
base_location/views/better_zip_view.xml

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record model="ir.ui.view" id="better_zip_form">
<field name="name">res.better.zip.form</field>
<field name="model">res.better.zip</field>
<field name="arch" type="xml">
<form string="City">
<group col="4">
<group>
<field name="name"/>
<field name="city_id"
attrs="{'invisible': [('enforce_cities', '=', False)],'required': [('enforce_cities', '=', True)]}"/>
<field name="city"
attrs="{'invisible': [('enforce_cities', '=', True)]}"/>
<field name="country_id"/>
<field name="enforce_cities" invisible="1"/>
</group>
<group>
<field name="code"/>
<field name="state_id"
attrs="{'invisible': [('enforce_cities', '=', True)]}"/>
</group>
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="better_zip_tree">
<field name="name">res.better.zip.tree</field>
<field name="model">res.better.zip</field>
<field name="arch" type="xml">
<tree string="Cities">
<field name="name"/>
<field name="code"/>
<field name="city"/>
<field name="state_id"/>
<field name="country_id"/>
</tree>
</field>
</record>
<record id="view_better_zip_filter" model="ir.ui.view">
<field name="name">res.better.zip.select</field>
<field name="model">res.better.zip</field>
<field name="arch" type="xml">
<search string="Search city">
<field name="name"/>
<field name="code"/>
<field name="city"/>
<field name="state_id"/>
<field name="country_id"/>
<group expand="0" string="Group By">
<filter string="State" domain="[]"
context="{'group_by':'state_id'}"/>
<filter string="Country" domain="[]"
context="{'group_by':'country_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_zip_tree" model="ir.actions.act_window">
<field name="name">Locations</field>
<field name="res_model">res.better.zip</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field ref="better_zip_tree" name="view_id"/>
<field name="search_view_id" ref="view_better_zip_filter"/>
</record>
<menuitem
name="Locations Management"
id="locations_root_menu"
parent="base.menu_custom"
/>
<menuitem
name="Locations"
id="locations_menu"
parent="locations_root_menu"
action="action_zip_tree"
/>
</odoo>

63
base_location/views/res_city_view.xml

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_city_tree_inherit" model="ir.ui.view">
<field name="model">res.city</field>
<field name="inherit_id" ref="base_address_city.view_city_tree"/>
<field name="arch" type="xml">
<tree position="attributes">
<attribute name="editable"/>
</tree>
<field name="zipcode" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="zipcode" position="after">
<field name="zip_ids" widget="many2many_tags"/>
</field>
</field>
</record>
<record id="view_city_form" model="ir.ui.view">
<field name="model">res.city</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="country_id"/>
<field name="state_id"/>
</group>
<notebook>
<page name="zips" string="Zips">
<field name="zip_ids"/>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_res_city_full" model="ir.actions.act_window">
<field name="name">Cities</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.city</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids"
eval="[(5,0,0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('base_address_city.view_city_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('view_city_form')})]"/>
<field name="help">
Display and manage the list of all cities that can be assigned to
your partner records. Note that an option can be set on each country separately
to enforce any address of it to have a city in this list.
</field>
</record>
<menuitem
name="Cities"
id="locations_menu_cities"
parent="contacts.menu_localisation"
action="action_res_city_full"
sequence="4"
/>
</odoo>

56
base_location/views/res_city_zip_view.xml

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record model="ir.ui.view" id="city_zip_form">
<field name="name">res.city.zip.form</field>
<field name="model">res.city.zip</field>
<field name="arch" type="xml">
<form string="Zip">
<group>
<field name="name"/>
<field name="city_id"/>
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="city_zip_tree">
<field name="name">res.city.zip.tree</field>
<field name="model">res.city.zip</field>
<field name="arch" type="xml">
<tree string="Zips" editable="top">
<field name="name"/>
<field name="city_id"/>
</tree>
</field>
</record>
<record id="view_city_zip_filter" model="ir.ui.view">
<field name="name">res.city.zip.select</field>
<field name="model">res.city.zip</field>
<field name="arch" type="xml">
<search string="Search zip">
<field name="name"/>
<field name="city_id"/>
</search>
</field>
</record>
<record id="action_zip_tree" model="ir.actions.act_window">
<field name="name">Locations</field>
<field name="res_model">res.city.zip</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field ref="city_zip_tree" name="view_id"/>
<field name="search_view_id" ref="view_city_zip_filter"/>
</record>
<menuitem
name="Zips"
id="locations_menu_zips"
parent="contacts.menu_localisation"
action="action_zip_tree"
sequence="5"
/>
</odoo>

0
base_location/views/company_view.xml → base_location/views/res_company_view.xml

2
base_location/views/res_country_view.xml

@ -22,7 +22,7 @@
icon="fa-globe" icon="fa-globe"
type="action" type="action"
context="{'default_country_id': active_id, 'search_default_country_id': active_id}" context="{'default_country_id': active_id, 'search_default_country_id': active_id}"
string="Locations">
string="Zips">
</button> </button>
</xpath> </xpath>
</field> </field>

1
base_location/views/partner_view.xml → base_location/views/res_partner_view.xml

@ -23,4 +23,3 @@
</record> </record>
</odoo> </odoo>

26
base_location/views/state_view.xml

@ -1,26 +0,0 @@
<?xml version="1.0"?>
<odoo>
<!-- Add cities to the State form -->
<record model="ir.ui.view" id="view_country_state_form2">
<field name="name">view_country_state_form2</field>
<field name="model">res.country.state</field>
<field name="inherit_id" ref="base.view_country_state_form"/>
<field name="arch" type="xml">
<field name="country_id" position="after">
<field name="better_zip_ids"
context="{'country_id': country_id}"
colspan="2"
nolabel="1">
<tree editable="top">
<field name="name"/>
<field name="code"/>
<field name="city"/>
<field name="country_id"/>
</tree>
</field>
</field>
</field>
</record>
</odoo>
Loading…
Cancel
Save