diff --git a/spp_demo/__manifest__.py b/spp_demo/__manifest__.py
index 84025f81..b8fc5b4f 100644
--- a/spp_demo/__manifest__.py
+++ b/spp_demo/__manifest__.py
@@ -18,6 +18,7 @@
"spp_vocabulary",
"queue_job",
"spp_security",
+ "spp_area",
],
"external_dependencies": {
"python": ["faker"],
@@ -34,6 +35,7 @@
"views/demo_data_generator_view.xml",
# Wizards
"wizard/apps_wizard_view.xml",
+ "wizard/demo_area_loader_view.xml",
],
"assets": {},
"demo": [],
diff --git a/spp_demo/data/countries/lka/area_kinds.xml b/spp_demo/data/countries/lka/area_kinds.xml
new file mode 100644
index 00000000..068c02ee
--- /dev/null
+++ b/spp_demo/data/countries/lka/area_kinds.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Province
+
+
+
+ District
+
+
+
+
+ DS Division
+
+
+
+
+ GN Division
+
+
+
diff --git a/spp_demo/data/countries/lka/areas.xml b/spp_demo/data/countries/lka/areas.xml
new file mode 100644
index 00000000..66bed174
--- /dev/null
+++ b/spp_demo/data/countries/lka/areas.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+ Western Province
+ LK-1
+
+
+
+
+ Southern Province
+ LK-3
+
+
+
+
+ Central Province
+ LK-2
+
+
+
+
+
+ Colombo
+ LK-11
+
+
+
+
+
+ Gampaha
+ LK-12
+
+
+
+
+
+ Kalutara
+ LK-13
+
+
+
+
+
+
+ Galle
+ LK-31
+
+
+
+
+
+ Matara
+ LK-32
+
+
+
+
+
+
+ Kandy
+ LK-21
+
+
+
+
+
+
+ Colombo
+ LK-1103
+
+
+
+
+
+ Dehiwala Mount Lavinia
+ LK-1106
+
+
+
+
+
+ Moratuwa
+ LK-1107
+
+
+
+
+
+ Kolonnawa
+ LK-1108
+
+
+
+
+
+
+ Galle Four Gravets
+ LK-3109
+
+
+
+
+
+ Hikkaduwa
+ LK-3110
+
+
+
+
+
+
+ Kandy Four Gravets
+ LK-2105
+
+
+
+
+
+
+ Fort
+ LK-1103-001
+
+
+
+
+
+ Pettah
+ LK-1103-002
+
+
+
+
+
+ Slave Island
+ LK-1103-003
+
+
+
+
+
+
+ Dehiwala East
+ LK-1106-001
+
+
+
+
+
+ Mount Lavinia
+ LK-1106-002
+
+
+
+
+
+
+ Galle Fort
+ LK-3109-001
+
+
+
+
diff --git a/spp_demo/data/countries/phl/area_kinds.xml b/spp_demo/data/countries/phl/area_kinds.xml
new file mode 100644
index 00000000..cc3ca60d
--- /dev/null
+++ b/spp_demo/data/countries/phl/area_kinds.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Region
+
+
+
+ Province
+
+
+
+
+ City/Municipality
+
+
+
+
+ Barangay
+
+
+
diff --git a/spp_demo/data/countries/phl/areas.xml b/spp_demo/data/countries/phl/areas.xml
new file mode 100644
index 00000000..6f25bc10
--- /dev/null
+++ b/spp_demo/data/countries/phl/areas.xml
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+ National Capital Region
+ PH-00
+
+
+
+
+ CALABARZON
+ PH-40
+
+
+
+
+
+
+ Metro Manila
+ PH-00-MM
+
+
+
+
+
+ Cavite
+ PH-40-21
+
+
+
+
+
+ Laguna
+ PH-40-34
+
+
+
+
+
+ Rizal
+ PH-40-58
+
+
+
+
+
+
+ Quezon City
+ PH-00-74
+
+
+
+
+
+ City of Manila
+ PH-00-39
+
+
+
+
+
+ Makati City
+ PH-00-38
+
+
+
+
+
+ Taguig City
+ PH-00-79
+
+
+
+
+
+ Pasig City
+ PH-00-76
+
+
+
+
+
+
+ Calamba City
+ PH-40-34-08
+
+
+
+
+
+ Santa Rosa City
+ PH-40-34-26
+
+
+
+
+
+ San Pablo City
+ PH-40-34-24
+
+
+
+
+
+ Antipolo City
+ PH-40-58-01
+
+
+
+
+
+ Bacoor City
+ PH-40-21-03
+
+
+
+
+
+ Dasmarinas City
+ PH-40-21-09
+
+
+
+
+
+
+ Loyola Heights
+ PH-00-74-061
+
+
+
+
+
+ Commonwealth
+ PH-00-74-028
+
+
+
+
+
+ Fairview
+ PH-00-74-044
+
+
+
+
+
+
+ Poblacion
+ PH-00-38-020
+
+
+
+
+
+ Bel-Air
+ PH-00-38-004
+
+
+
+
+
+
+ Real
+ PH-40-34-08-034
+
+
+
+
+
+ Crossing
+ PH-40-34-08-011
+
+
+
+
+
+
+ Balibago
+ PH-40-34-26-002
+
+
+
+
+
+ Tagapo
+ PH-40-34-26-017
+
+
+
+
+
+
+ Dela Paz
+ PH-40-58-01-005
+
+
+
+
+
+ San Roque
+ PH-40-58-01-015
+
+
+
+
diff --git a/spp_demo/data/countries/tgo/area_kinds.xml b/spp_demo/data/countries/tgo/area_kinds.xml
new file mode 100644
index 00000000..1a9f1573
--- /dev/null
+++ b/spp_demo/data/countries/tgo/area_kinds.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Region
+
+
+
+ Prefecture
+
+
+
+
+ Canton
+
+
+
+
+ Village
+
+
+
diff --git a/spp_demo/data/countries/tgo/areas.xml b/spp_demo/data/countries/tgo/areas.xml
new file mode 100644
index 00000000..00e8dcaf
--- /dev/null
+++ b/spp_demo/data/countries/tgo/areas.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+ Maritime
+ TG-M
+
+
+
+
+ Plateaux
+ TG-P
+
+
+
+
+ Centrale
+ TG-C
+
+
+
+
+
+ Golfe
+ TG-M-GOL
+
+
+
+
+
+ Lacs
+ TG-M-LAC
+
+
+
+
+
+ Vo
+ TG-M-VO
+
+
+
+
+
+ Zio
+ TG-M-ZIO
+
+
+
+
+
+
+ Ogou
+ TG-P-OGO
+
+
+
+
+
+ Kloto
+ TG-P-KLO
+
+
+
+
+
+
+ Tchaoudjo
+ TG-C-TCH
+
+
+
+
+
+
+ Lome Commune
+ TG-M-GOL-LOM
+
+
+
+
+
+ Aflao Sagbado
+ TG-M-GOL-AFL
+
+
+
+
+
+ Baguida
+ TG-M-GOL-BAG
+
+
+
+
+
+
+ Kpalime
+ TG-P-KLO-KPA
+
+
+
+
+
+
+ Sokode
+ TG-C-TCH-SOK
+
+
+
+
+
+
+ Tokoin
+ TG-M-GOL-LOM-TOK
+
+
+
+
+
+ Be
+ TG-M-GOL-LOM-BE
+
+
+
+
+
+ Nyekonakpoe
+ TG-M-GOL-LOM-NYE
+
+
+
+
+
+ Adidogome
+ TG-M-GOL-LOM-ADI
+
+
+
+
+
+
+ Baguida Centre
+ TG-M-GOL-BAG-CEN
+
+
+
+
+
+
+ Kpalime Centre
+ TG-P-KLO-KPA-CEN
+
+
+
+
+
+ Tove
+ TG-P-KLO-KPA-TOV
+
+
+
+
diff --git a/spp_demo/data/shapes/lka_curated.geojson b/spp_demo/data/shapes/lka_curated.geojson
new file mode 100644
index 00000000..667498f4
--- /dev/null
+++ b/spp_demo/data/shapes/lka_curated.geojson
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:25dd1a0ddda148ead42b2c4ac2fd23aac17970a4ba98aab7d85834ffdf184e6d
+size 4284
diff --git a/spp_demo/data/shapes/phl_curated.geojson b/spp_demo/data/shapes/phl_curated.geojson
new file mode 100644
index 00000000..6f8f003e
--- /dev/null
+++ b/spp_demo/data/shapes/phl_curated.geojson
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b01896e654357c0a9dab66c6a5464f64ba9399ad492042eb996df56637ace15
+size 625794
diff --git a/spp_demo/data/shapes/tgo_curated.geojson b/spp_demo/data/shapes/tgo_curated.geojson
new file mode 100644
index 00000000..768a099f
--- /dev/null
+++ b/spp_demo/data/shapes/tgo_curated.geojson
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54c8d790e8df5f06af922c8dd24a5b00af791bc78b64f7d178bdcb3680247574
+size 4772
diff --git a/spp_demo/models/__init__.py b/spp_demo/models/__init__.py
index 73fd391a..6acaa1d2 100644
--- a/spp_demo/models/__init__.py
+++ b/spp_demo/models/__init__.py
@@ -4,3 +4,4 @@
from . import demo_data_generator
from . import demo_data_generation_log
from . import demo_stories
+from . import demo_area_loader
diff --git a/spp_demo/models/demo_area_loader.py b/spp_demo/models/demo_area_loader.py
new file mode 100644
index 00000000..d6a8352b
--- /dev/null
+++ b/spp_demo/models/demo_area_loader.py
@@ -0,0 +1,291 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+"""
+Demo Area Loader
+
+Handles loading curated geographic data (areas and shapes) for demo environments.
+Supports multiple countries with conditional GIS shape loading.
+"""
+
+import json
+import logging
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools.misc import file_path
+
+_logger = logging.getLogger(__name__)
+
+
+class DemoAreaLoader(models.TransientModel):
+ """Load curated demo areas for supported countries."""
+
+ _name = "spp.demo.area.loader"
+ _description = "Demo Area Data Loader"
+
+ SUPPORTED_COUNTRIES = [
+ ("phl", "Philippines"),
+ ("lka", "Sri Lanka"),
+ ("tgo", "Togo"),
+ ]
+
+ country_code = fields.Selection(
+ selection=SUPPORTED_COUNTRIES,
+ string="Country",
+ required=True,
+ default="phl",
+ )
+
+ load_shapes = fields.Boolean(
+ string="Load Shapes (GIS)",
+ default=True,
+ help="Load polygon shapes for GIS visualization. Requires spp_gis module to be installed.",
+ )
+
+ @api.model
+ def _is_gis_installed(self):
+ """Check if spp_gis module is installed."""
+ return bool(self.env["ir.module.module"].search([("name", "=", "spp_gis"), ("state", "=", "installed")]))
+
+ @api.model
+ def _get_country_name(self, country_code):
+ """Get the display name for a country code."""
+ for code, name in self.SUPPORTED_COUNTRIES:
+ if code == country_code:
+ return name
+ return country_code.upper()
+
+ def action_load_areas(self):
+ """Load area kinds and areas for the selected country.
+
+ This method:
+ 1. Loads area kinds (hierarchy definition) from XML via ir.model.data
+ 2. Loads curated areas from XML via ir.model.data
+ 3. Optionally loads GeoJSON shapes if spp_gis is installed
+
+ Returns:
+ dict: Notification action showing results
+ """
+ self.ensure_one()
+ country_code = self.country_code
+
+ # Check if area data files exist
+ try:
+ file_path(f"spp_demo/data/countries/{country_code}/area_kinds.xml")
+ file_path(f"spp_demo/data/countries/{country_code}/areas.xml")
+ except FileNotFoundError as exc:
+ raise UserError(
+ _("Area data files not found for country: %s") % self._get_country_name(country_code)
+ ) from exc
+
+ # Count existing areas before loading
+ existing_count = self.env["spp.area"].search_count([])
+
+ # Load XML data files using convert_file
+ self._load_xml_data(country_code)
+
+ # Count new areas
+ new_count = self.env["spp.area"].search_count([])
+ areas_created = new_count - existing_count
+
+ # Load shapes if requested and GIS is available
+ shapes_loaded = 0
+ if self.load_shapes and self._is_gis_installed():
+ shapes_loaded = self._load_shapes(country_code)
+
+ # Build notification message
+ country_name = self._get_country_name(country_code)
+ message = _("Successfully loaded geographic data for %(country)s:\n- %(areas)d areas created/updated\n") % {
+ "country": country_name,
+ "areas": areas_created,
+ }
+
+ if shapes_loaded > 0:
+ message += _("- %(shapes)d polygon shapes loaded\n") % {"shapes": shapes_loaded}
+ elif self.load_shapes and not self._is_gis_installed():
+ message += _("- Shapes not loaded (spp_gis not installed)\n")
+
+ return {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "title": _("Geographic Data Loaded"),
+ "message": message,
+ "type": "success",
+ "sticky": False,
+ },
+ }
+
+ def _load_xml_data(self, country_code):
+ """Load area kinds and areas XML files for a country.
+
+ The XML files are structured to be loaded via Odoo's data loading mechanism.
+ We use convert_file to process them properly with noupdate handling.
+
+ Args:
+ country_code: ISO 3166-1 alpha-3 country code (lowercase)
+ """
+ from odoo.tools import convert_file
+
+ # Load area kinds first (hierarchy definition)
+ area_kinds_path = f"data/countries/{country_code}/area_kinds.xml"
+ try:
+ convert_file(
+ self.env,
+ "spp_demo",
+ area_kinds_path,
+ idref={},
+ mode="init",
+ noupdate=True,
+ )
+ _logger.info("Loaded area kinds for %s", country_code)
+ except Exception as e:
+ _logger.warning("Could not load area kinds for %s: %s", country_code, e)
+
+ # Load areas (the actual geographic entities)
+ areas_path = f"data/countries/{country_code}/areas.xml"
+ try:
+ convert_file(
+ self.env,
+ "spp_demo",
+ areas_path,
+ idref={},
+ mode="init",
+ noupdate=False,
+ )
+ _logger.info("Loaded areas for %s", country_code)
+ except Exception as e:
+ _logger.warning("Could not load areas for %s: %s", country_code, e)
+
+ def _load_shapes(self, country_code):
+ """Load GeoJSON shapes for areas.
+
+ Reads the curated GeoJSON file and updates matching spp.area records
+ with their polygon geometries.
+
+ Args:
+ country_code: ISO 3166-1 alpha-3 country code (lowercase)
+
+ Returns:
+ int: Number of shapes successfully loaded
+ """
+ try:
+ geojson_path = file_path(f"spp_demo/data/shapes/{country_code}_curated.geojson")
+ except FileNotFoundError:
+ _logger.warning("GeoJSON file not found for %s", country_code)
+ return 0
+
+ try:
+ with open(geojson_path, encoding="utf-8") as f:
+ geojson_data = json.load(f)
+ except (OSError, json.JSONDecodeError) as e:
+ _logger.warning("Could not read GeoJSON file for %s: %s", country_code, e)
+ return 0
+
+ features = geojson_data.get("features", [])
+ shapes_loaded = 0
+
+ for feature in features:
+ properties = feature.get("properties", {})
+ geometry = feature.get("geometry")
+
+ if not properties.get("code") or not geometry:
+ continue
+
+ area_code = properties["code"]
+
+ # Find matching area by code
+ area = self.env["spp.area"].search([("code", "=", area_code)], limit=1)
+ if not area:
+ _logger.debug("Area not found for code: %s", area_code)
+ continue
+
+ # Check if area has geo_polygon field (spp_gis installed)
+ if "geo_polygon" not in area._fields:
+ _logger.debug("geo_polygon field not available on spp.area")
+ break
+
+ # Convert GeoJSON geometry to WKT format for PostGIS
+ try:
+ wkt = self._geojson_to_wkt(geometry)
+ if wkt:
+ area.write({"geo_polygon": wkt})
+ shapes_loaded += 1
+ except Exception as e:
+ _logger.warning("Could not set shape for %s: %s", area_code, e)
+
+ _logger.info("Loaded %d shapes for %s", shapes_loaded, country_code)
+ return shapes_loaded
+
+ def _geojson_to_wkt(self, geometry):
+ """Convert GeoJSON geometry to WKT format.
+
+ Supports Polygon and MultiPolygon geometries.
+
+ Args:
+ geometry: GeoJSON geometry dict
+
+ Returns:
+ str: WKT string or None if conversion fails
+ """
+ geom_type = geometry.get("type")
+ coordinates = geometry.get("coordinates")
+
+ if not geom_type or not coordinates:
+ return None
+
+ if geom_type == "Polygon":
+ return self._polygon_to_wkt(coordinates)
+ elif geom_type == "MultiPolygon":
+ polygons = [self._polygon_to_wkt(poly) for poly in coordinates]
+ polygons = [p for p in polygons if p]
+ if polygons:
+ # Extract just the coordinate parts from each polygon WKT
+ poly_coords = [p.replace("POLYGON", "").strip() for p in polygons]
+ return f"MULTIPOLYGON({','.join(poly_coords)})"
+ return None
+
+ def _polygon_to_wkt(self, coordinates):
+ """Convert polygon coordinates to WKT.
+
+ Args:
+ coordinates: List of rings (first is exterior, rest are holes)
+
+ Returns:
+ str: WKT POLYGON string
+ """
+ if not coordinates:
+ return None
+
+ rings = []
+ for ring in coordinates:
+ points = [f"{p[0]} {p[1]}" for p in ring]
+ rings.append(f"({','.join(points)})")
+
+ return f"POLYGON({','.join(rings)})"
+
+ @api.model
+ def load_country_areas(self, country_code, load_shapes=True):
+ """Programmatic method to load areas for a country.
+
+ This method can be called from other modules or scripts
+ to load geographic data without going through the wizard.
+
+ Args:
+ country_code: ISO 3166-1 alpha-3 country code (lowercase)
+ load_shapes: Whether to load GIS shapes (default True)
+
+ Returns:
+ dict: Result with counts of loaded data
+ """
+ loader = self.create({"country_code": country_code, "load_shapes": load_shapes})
+ loader._load_xml_data(country_code)
+
+ shapes_loaded = 0
+ if load_shapes and self._is_gis_installed():
+ shapes_loaded = loader._load_shapes(country_code)
+
+ return {
+ "country_code": country_code,
+ "shapes_loaded": shapes_loaded,
+ "gis_available": self._is_gis_installed(),
+ }
diff --git a/spp_demo/models/demo_data_generator.py b/spp_demo/models/demo_data_generator.py
index 1a65438c..c0a26012 100644
--- a/spp_demo/models/demo_data_generator.py
+++ b/spp_demo/models/demo_data_generator.py
@@ -69,20 +69,11 @@ def _default_days_back(self):
is_remember_settings = fields.Boolean(string="Remember Settings", default=False)
number_of_groups = fields.Integer(string="Number of Groups", default=_default_number_of_groups, required=True)
members_range_from = fields.Integer(
- string="Members per Group (From)",
- default=_default_members_range_from,
- required=True,
- )
- members_range_to = fields.Integer(
- string="Members per Group (To)",
- default=_default_members_range_to,
- required=True,
+ string="Members per Group (From)", default=_default_members_range_from, required=True
)
+ members_range_to = fields.Integer(string="Members per Group (To)", default=_default_members_range_to, required=True)
locale_origin = fields.Many2one(
- "res.country",
- string="Locale Origin",
- required=True,
- default=_default_locale_origin,
+ "res.country", string="Locale Origin", required=True, default=_default_locale_origin
)
locale_origin_faker_locale = fields.Char(string="Locale Origin Faker Locale", related="locale_origin.faker_locale")
batch_size = fields.Integer(string="Batch Size", default=_default_batch_size, required=True)
@@ -93,12 +84,7 @@ def _default_days_back(self):
help="Generate data within the last N days. Used for time-based demo data generation.",
)
state = fields.Selection(
- [
- ("draft", "Draft"),
- ("in_progress", "In Progress"),
- ("completed", "Completed"),
- ("cancelled", "Cancelled"),
- ],
+ [("draft", "Draft"), ("in_progress", "In Progress"), ("completed", "Completed"), ("cancelled", "Cancelled")],
string="State",
default="draft",
required=True,
@@ -114,6 +100,23 @@ def _default_days_back(self):
percentage_with_ids = fields.Integer(string="% with IDs", default=100, required=True)
percentage_with_gps = fields.Integer(string="% with GPS Coordinates", default=100, required=True)
+ # Geographic demo data settings
+ demo_country = fields.Selection(
+ [
+ ("phl", "Philippines"),
+ ("lka", "Sri Lanka"),
+ ("tgo", "Togo"),
+ ],
+ string="Demo Country",
+ default="phl",
+ help="Country for geographic demo data (areas, shapes)",
+ )
+ load_geographic_data = fields.Boolean(
+ string="Load Geographic Data",
+ default=True,
+ help="Load curated areas for the selected country during demo generation",
+ )
+
is_locked = fields.Boolean(string="Locked", default=False)
locked_reason = fields.Text(string="Locked Reason")
@@ -154,6 +157,11 @@ def _compute_generation_log_count(self):
def generate_demo_data(self):
self.ensure_one()
+
+ # Load geographic data if enabled
+ if self.load_geographic_data and self.demo_country:
+ self._load_geographic_data()
+
faker_code = self.locale_origin.faker_locale or "en_US"
fake = Faker(faker_code)
if self.members_range_from > self.members_range_to:
@@ -193,6 +201,22 @@ def generate_demo_data(self):
},
}
+ def _load_geographic_data(self):
+ """Load curated geographic data for the selected country.
+
+ Uses the DemoAreaLoader to load area kinds, areas, and optionally
+ GIS polygon shapes for the demo country.
+ """
+ if not self.demo_country:
+ return
+
+ loader = self.env["spp.demo.area.loader"]
+ try:
+ loader.load_country_areas(self.demo_country, load_shapes=True)
+ _logger.info("Loaded geographic data for %s", self.demo_country)
+ except Exception as e:
+ _logger.warning("Could not load geographic data for %s: %s", self.demo_country, e)
+
def _generate_demo_data(self, fake):
group = self.generate_groups(fake)
num_members = fake.random_int(self.members_range_from, self.members_range_to)
@@ -207,6 +231,11 @@ def _generate_demo_data(self, fake):
is_head_member = True
individual = self.generate_individuals(fake)
+
+ # Members inherit area from their group
+ if group.area_id and individual.area_id != group.area_id:
+ individual.write({"area_id": group.area_id.id})
+
membership_vals = self.get_group_membership_vals(fake, group, individual)
if is_head_member and not head_membership:
have_head_member = True
@@ -246,11 +275,37 @@ def _process_batch(self, batch):
self._generate_demo_data(fake)
def _mark_done(self):
+ # Refresh GIS reports if the module is installed
+ self._refresh_gis_reports()
+
self.state = "completed"
self.is_locked = False
message = "The data generation has been completed."
self.locked_reason = message
+ def _refresh_gis_reports(self):
+ """Refresh all active GIS reports so map data is available immediately.
+
+ Only runs if spp_gis_report is installed (not a hard dependency).
+ """
+ if "spp.gis.report" not in self.env:
+ return
+
+ GISReport = self.env["spp.gis.report"]
+ reports = GISReport.search([("active", "=", True)])
+
+ if not reports:
+ _logger.info("No active GIS reports found to refresh")
+ return
+
+ for report in reports:
+ report_name = report.name
+ try:
+ report._refresh_data()
+ _logger.info("Refreshed GIS report: %s", report_name)
+ except Exception:
+ _logger.exception("Failed to refresh GIS report: %s", report_name)
+
def generate_groups(self, fake):
group_vals = self.get_group_vals(fake)
group = self.env["res.partner"].create(group_vals)
@@ -306,6 +361,22 @@ def generate_individuals(self, fake):
self.create_gps_coordinates(fake, individual)
return individual
+ def _get_leaf_areas(self):
+ """Get leaf-level areas (areas with no children) for assignment.
+
+ Prefers the deepest level of the area hierarchy so that
+ GIS reports can aggregate upward through parent areas.
+
+ Returns:
+ recordset: spp.area records at the deepest available level
+ """
+ Area = self.env["spp.area"]
+ all_areas = Area.search([])
+ if not all_areas:
+ return all_areas
+ max_level = max(all_areas.mapped("area_level"))
+ return all_areas.filtered(lambda a: a.area_level == max_level)
+
def get_group_vals(self, fake):
registration_date = self.get_random_date(
fake,
@@ -332,6 +403,11 @@ def get_group_vals(self, fake):
if group_types:
group_vals["group_type_id"] = random.choice(group_types).id
+ # Assign area (prefer leaf-level for proper GIS report aggregation)
+ leaf_areas = self._get_leaf_areas()
+ if leaf_areas:
+ group_vals["area_id"] = random.choice(leaf_areas).id
+
return group_vals
def get_individual_vals(self, fake):
@@ -382,12 +458,11 @@ def get_individual_vals(self, fake):
if "email" in partner_fields:
individual_vals["email"] = fake.email()
- # District field (if exists)
- if "district" in partner_fields:
- # Try to get a random district from available districts
- districts = self.env["spp.district"].search([])
- if districts:
- individual_vals["district"] = random.choice(districts).id
+ # Area field — prefer leaf-level areas for proper GIS aggregation
+ if "area_id" in partner_fields:
+ leaf_areas = self._get_leaf_areas()
+ if leaf_areas:
+ individual_vals["area_id"] = random.choice(leaf_areas).id
# Birth place
if "birth_place" in partner_fields:
@@ -500,14 +575,55 @@ def lookup_gender_id(self, gender):
gender_code = VocabCode.get_code("urn:iso:std:iso:5218", iso_code)
if not gender_code:
gender_code = VocabCode.search(
- [
- ("namespace_uri", "=", "urn:iso:std:iso:5218"),
- ("display", "ilike", gender),
- ],
+ [("namespace_uri", "=", "urn:iso:std:iso:5218"), ("display", "ilike", gender)],
limit=1,
)
return gender_code.id if gender_code else False
+ def _get_area_for_profile(self, profile):
+ """Get area_id based on profile configuration.
+
+ Supports three ways to assign areas:
+ 1. area_ref: Direct XML ID reference (e.g., 'spp_demo.area_phl_ncr_quezon_city')
+ 2. area_kind: Pick random area of specified kind (e.g., 'municipality')
+ 3. Fallback: Pick any available area
+
+ Args:
+ profile: Dict with optional 'area_ref' or 'area_kind' keys
+
+ Returns:
+ int or False: area_id or False if no areas available
+ """
+ if not profile:
+ return False
+
+ # Priority 1: Explicit area reference
+ area_ref = profile.get("area_ref")
+ if area_ref:
+ area = self.env.ref(area_ref, raise_if_not_found=False)
+ if area:
+ return area.id
+
+ # Priority 2: Random area of specified kind
+ area_kind_name = profile.get("area_kind")
+ if area_kind_name:
+ # Search for area kind by name (case-insensitive partial match)
+ kind = self.env["spp.area.type"].search(
+ [("name", "ilike", area_kind_name)],
+ limit=1,
+ )
+ if kind:
+ areas = self.env["spp.area"].search([("area_type_id", "=", kind.id)])
+ if areas:
+ return random.choice(areas).id
+
+ # Priority 3: Any available area (limit search for performance)
+ areas = self.env["spp.area"].search([], limit=100)
+ if areas:
+ return random.choice(areas).id
+
+ return False
+
@api.model
def create_individual_from_params(self, name, gender, age, extra_vals=None):
"""Create an individual registrant from parameters.
@@ -630,10 +746,7 @@ def _log_generation_failure(
def get_id_type(self, target_type):
if self.id_type_ids:
id_type = self.env["spp.demo.data.id.types"].search(
- [
- ("target_type", "=", target_type),
- ("demo_data_generator_id", "=", self.id),
- ]
+ [("target_type", "=", target_type), ("demo_data_generator_id", "=", self.id)]
)
if id_type:
if len(self.id_type_ids) == 1:
@@ -901,10 +1014,7 @@ def create_ids(self, fake, registrant):
def get_bank_type(self, target_type):
if self.bank_type_ids:
bank_type = self.env["spp.demo.data.bank.types"].search(
- [
- ("target_type", "=", target_type),
- ("demo_data_generator_id", "=", self.id),
- ]
+ [("target_type", "=", target_type), ("demo_data_generator_id", "=", self.id)]
)
if bank_type:
if len(self.bank_type_ids) == 1:
@@ -962,10 +1072,7 @@ def generate_phone_number(self, fake):
attempt += 1
# Return None if we couldn't generate a valid phone number
- _logger.warning(
- "Failed to generate valid phone number after %s attempts. Returning None.",
- max_attempts,
- )
+ _logger.warning("Failed to generate valid phone number after %s attempts. Returning None.", max_attempts)
return None
def create_phone_numbers(self, fake, registrant):
@@ -1006,11 +1113,7 @@ def create_phone_numbers(self, fake, registrant):
self.env["spp.phone.number"].create(phone_vals_list)
if failed_count > 0:
- _logger.warning(
- "Failed to generate %d phone number(s) for registrant_id=%s",
- failed_count,
- registrant.id,
- )
+ _logger.warning("Failed to generate %d phone number(s) for registrant_id=%s", failed_count, registrant.id)
registrant.phone_number_ids_change()
@@ -1131,19 +1234,10 @@ def generate_stories(self):
if partner:
created_partners[story["id"]] = partner
- _logger.info(
- "Created demo story: %s (partner_id=%s)",
- story["id"],
- partner.id,
- )
+ _logger.info("Created demo story: %s (partner_id=%s)", story["id"], partner.id)
except Exception as e:
- _logger.error(
- "Error creating story '%s': %s",
- story.get("id", "unknown"),
- e,
- exc_info=True,
- )
+ _logger.error("Error creating story '%s': %s", story.get("id", "unknown"), e, exc_info=True)
_logger.info(f"Demo story generation completed. Created {len(created_partners)} stories.")
return created_partners
@@ -1200,13 +1294,11 @@ def _create_individual_story(self, story):
elif "email" in partner_fields:
vals["email"] = fake.email()
- # District field (if exists)
- if "district" in partner_fields:
- district_name = profile.get("district")
- if district_name:
- districts = self.env["spp.district"].search([("name", "=", district_name)])
- if districts:
- vals["district"] = districts[0].id
+ # Area field (if spp_area is installed)
+ if "area_id" in partner_fields:
+ area_id = self._get_area_for_profile(profile)
+ if area_id:
+ vals["area_id"] = area_id
# Birth place
if "birth_place" in partner_fields and profile.get("birth_place"):
diff --git a/spp_demo/models/demo_stories.py b/spp_demo/models/demo_stories.py
index 60cfa312..7e648af7 100644
--- a/spp_demo/models/demo_stories.py
+++ b/spp_demo/models/demo_stories.py
@@ -100,7 +100,8 @@
"farm_size_hectares": 2.5, # CEL: Input Subsidy eligibility
"farm_type": "crop",
"main_crop": "rice",
- "district": "Northern District",
+ "area_ref": "spp_demo.area_phl_quezon_city",
+ "area_kind": "municipality",
"marital_status": "married",
"household_size": 5,
},
@@ -113,30 +114,11 @@
"cel_check": "farm_size",
"days_back": 152,
},
- {
- "action": "enroll_program",
- "program": "Input Subsidy Program",
- "days_back": 150,
- },
+ {"action": "enroll_program", "program": "Input Subsidy Program", "days_back": 150},
{"action": "create_event", "event_type": "training", "days_back": 145},
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 120,
- },
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 90,
- },
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 60,
- },
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 120},
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 90},
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 60},
{"action": "graduate_program", "days_back": 30},
],
"demo_points": [
@@ -165,23 +147,9 @@
},
"journey": [
{"action": "register", "days_back": 120},
- {
- "action": "enroll_program",
- "program": "Cash Transfer Program",
- "days_back": 100,
- },
- {
- "action": "create_payment",
- "amount": 150,
- "status": "paid",
- "days_back": 70,
- },
- {
- "action": "create_payment",
- "amount": 150,
- "status": "failed",
- "days_back": 40,
- },
+ {"action": "enroll_program", "program": "Cash Transfer Program", "days_back": 100},
+ {"action": "create_payment", "amount": 150, "status": "paid", "days_back": 70},
+ {"action": "create_payment", "amount": 150, "status": "failed", "days_back": 40},
{
"action": "create_grm_ticket",
"title": "Payment not received",
@@ -189,22 +157,9 @@
"days_back": 38,
},
{"action": "assign_ticket", "days_back": 35},
- {
- "action": "add_ticket_note",
- "note": "Investigation: Bank details were incorrect",
- "days_back": 32,
- },
- {
- "action": "resolve_ticket",
- "resolution": "Bank details corrected",
- "days_back": 30,
- },
- {
- "action": "create_payment",
- "amount": 150,
- "status": "paid",
- "days_back": 25,
- },
+ {"action": "add_ticket_note", "note": "Investigation: Bank details were incorrect", "days_back": 32},
+ {"action": "resolve_ticket", "resolution": "Bank details corrected", "days_back": 30},
+ {"action": "create_payment", "amount": 150, "status": "paid", "days_back": 25},
],
"demo_points": [
"GRM ticket with full conversation history",
@@ -237,16 +192,8 @@
"cel_check": "age_retirement",
"days_back": 182,
},
- {
- "action": "enroll_program",
- "program": "Elderly Pension",
- "days_back": 180,
- },
- {
- "action": "enroll_program",
- "program": "Food Assistance",
- "days_back": 175,
- },
+ {"action": "enroll_program", "program": "Elderly Pension", "days_back": 180},
+ {"action": "enroll_program", "program": "Food Assistance", "days_back": 175},
{
"action": "create_payment",
"amount": 100,
@@ -297,7 +244,8 @@
"farm_size_hectares": 8.0, # CEL: Large livestock farm
"farm_type": "livestock",
"main_livestock": "dairy",
- "district": "Central District",
+ "area_ref": "spp_demo.area_phl_calamba",
+ "area_kind": "municipality",
"marital_status": "married",
"household_size": 6,
"role": "cooperative_chairman",
@@ -306,27 +254,10 @@
{"action": "register", "days_back": 365},
{"action": "add_farm_details", "comprehensive": True, "days_back": 360},
{"action": "register_cooperative_leader", "days_back": 350},
- {
- "action": "enroll_program",
- "program": "Livestock Improvement Program",
- "days_back": 300,
- },
- {
- "action": "create_event",
- "event_type": "extension_visit",
- "days_back": 250,
- },
- {
- "action": "create_event",
- "event_type": "extension_visit",
- "days_back": 200,
- },
- {
- "action": "create_in_kind",
- "item": "Improved Breed Cattle",
- "quantity": 2,
- "days_back": 150,
- },
+ {"action": "enroll_program", "program": "Livestock Improvement Program", "days_back": 300},
+ {"action": "create_event", "event_type": "extension_visit", "days_back": 250},
+ {"action": "create_event", "event_type": "extension_visit", "days_back": 200},
+ {"action": "create_in_kind", "item": "Improved Breed Cattle", "quantity": 2, "days_back": 150},
],
"demo_points": [
"Detailed farm profile (livestock, assets, land records)",
@@ -349,7 +280,8 @@
"farm_size_hectares": 3.0, # CEL: Youth farmer eligibility
"farm_type": "crop",
"main_crop": "mixed_vegetables",
- "district": "Eastern District",
+ "area_ref": "spp_demo.area_phl_antipolo",
+ "area_kind": "municipality",
"marital_status": "single",
"household_size": 2,
"registration_channel": "mobile_app",
@@ -358,17 +290,9 @@
"journey": [
{"action": "register", "channel": "mobile_app", "days_back": 90},
{"action": "add_farm_details", "with_gps": True, "days_back": 85},
- {
- "action": "apply_program",
- "program": "Input Subsidy Program",
- "days_back": 80,
- },
+ {"action": "apply_program", "program": "Input Subsidy Program", "days_back": 80},
{"action": "verify_eligibility", "days_back": 75},
- {
- "action": "enroll_program",
- "program": "Input Subsidy Program",
- "days_back": 70,
- },
+ {"action": "enroll_program", "program": "Input Subsidy Program", "days_back": 70},
{"action": "create_in_kind", "item": "Inputs Package", "days_back": 45},
],
"demo_points": [
@@ -394,7 +318,8 @@
"farm_size": 2.0,
"farm_size_hectares": 2.0, # CEL: Household farm size
"child_count": 3, # CEL: Child benefit eligibility
- "district": "Southern District",
+ "area_ref": "spp_demo.area_phl_santa_rosa",
+ "area_kind": "municipality",
},
"journey": [
{"action": "register_household", "days_back": 150},
@@ -405,23 +330,9 @@
"cel_check": "member_count",
"days_back": 142,
},
- {
- "action": "enroll_program",
- "program": "Child Support Grant",
- "days_back": 140,
- },
- {
- "action": "create_payment",
- "amount": 300,
- "status": "paid",
- "days_back": 100,
- },
- {
- "action": "create_payment",
- "amount": 300,
- "status": "paid",
- "days_back": 10,
- },
+ {"action": "enroll_program", "program": "Child Support Grant", "days_back": 140},
+ {"action": "create_payment", "amount": 300, "status": "paid", "days_back": 100},
+ {"action": "create_payment", "amount": 300, "status": "paid", "days_back": 10},
],
"demo_points": [
"Household with multiple members",
@@ -446,7 +357,8 @@
"vulnerability": ["single_parent", "low_income", "female_headed"],
"vulnerability_score": 80, # CEL: High vulnerability - single parent household
"child_count": 3, # CEL: Child benefit eligibility
- "district": "Western District",
+ "area_ref": "spp_demo.area_phl_makati",
+ "area_kind": "municipality",
},
"journey": [
{"action": "register_household", "days_back": 180},
@@ -457,28 +369,10 @@
"cel_check": "member_count",
"days_back": 162,
},
- {
- "action": "enroll_program",
- "program": "Child Support Grant",
- "days_back": 160,
- },
- {
- "action": "enroll_program",
- "program": "Food Assistance",
- "days_back": 155,
- },
- {
- "action": "create_payment",
- "amount": 350,
- "status": "paid",
- "days_back": 120,
- },
- {
- "action": "create_payment",
- "amount": 350,
- "status": "paid",
- "days_back": 60,
- },
+ {"action": "enroll_program", "program": "Child Support Grant", "days_back": 160},
+ {"action": "enroll_program", "program": "Food Assistance", "days_back": 155},
+ {"action": "create_payment", "amount": 350, "status": "paid", "days_back": 120},
+ {"action": "create_payment", "amount": 350, "status": "paid", "days_back": 60},
],
"demo_points": [
"Female-headed household",
@@ -497,18 +391,8 @@
"head": {"name": "Jose Reyes Sr", "gender": "male", "age": 72},
"spouse": {"name": "Carmen Reyes", "gender": "female", "age": 68},
"adults": [
- {
- "name": "Miguel Reyes",
- "gender": "male",
- "age": 45,
- "relation": "son",
- },
- {
- "name": "Teresa Reyes",
- "gender": "female",
- "age": 42,
- "relation": "daughter-in-law",
- },
+ {"name": "Miguel Reyes", "gender": "male", "age": 45, "relation": "son"},
+ {"name": "Teresa Reyes", "gender": "female", "age": 42, "relation": "daughter-in-law"},
],
"children": [
{"name": "Jose Reyes Jr", "gender": "male", "age": 18},
@@ -519,7 +403,8 @@
"farm_size": 5.0,
"farm_size_hectares": 5.0, # CEL: Multi-generational household farm
"child_count": 3, # CEL: Children under 18 (excluding 18-year-old)
- "district": "Northern District",
+ "area_ref": "spp_demo.area_phl_quezon_city",
+ "area_kind": "municipality",
"vulnerability": ["elderly_members"],
},
"journey": [
@@ -531,22 +416,14 @@
"cel_check": "age_retirement",
"days_back": 352,
},
- {
- "action": "enroll_program",
- "program": "Elderly Pension",
- "days_back": 350,
- },
+ {"action": "enroll_program", "program": "Elderly Pension", "days_back": 350},
{
"action": "verify_eligibility",
"program": "Child Support Grant",
"cel_check": "member_count",
"days_back": 342,
},
- {
- "action": "enroll_program",
- "program": "Child Support Grant",
- "days_back": 340,
- },
+ {"action": "enroll_program", "program": "Child Support Grant", "days_back": 340},
{
"action": "create_payment",
"amount": 200,
@@ -604,7 +481,8 @@
"child_count": 3, # CEL: Children under 18 (Xiao, Yan, Bo)
"farm_type": "crop",
"main_crop": "rice",
- "district": "Eastern District",
+ "area_ref": "spp_demo.area_phl_antipolo",
+ "area_kind": "municipality",
},
"journey": [
{"action": "register_household", "days_back": 200},
@@ -616,35 +494,17 @@
"cel_check": "farm_size",
"days_back": 182,
},
- {
- "action": "enroll_program",
- "program": "Input Subsidy Program",
- "days_back": 180,
- },
+ {"action": "enroll_program", "program": "Input Subsidy Program", "days_back": 180},
{
"action": "verify_eligibility",
"program": "Child Support Grant",
"cel_check": "member_count",
"days_back": 177,
},
- {
- "action": "enroll_program",
- "program": "Child Support Grant",
- "days_back": 175,
- },
+ {"action": "enroll_program", "program": "Child Support Grant", "days_back": 175},
{"action": "create_in_kind", "item": "Inputs Package", "days_back": 150},
- {
- "action": "create_payment",
- "amount": 450,
- "status": "paid",
- "days_back": 140,
- },
- {
- "action": "create_payment",
- "amount": 450,
- "status": "paid",
- "days_back": 80,
- },
+ {"action": "create_payment", "amount": 450, "status": "paid", "days_back": 140},
+ {"action": "create_payment", "amount": 450, "status": "paid", "days_back": 80},
],
"demo_points": [
"Large family (7 members)",
@@ -665,7 +525,8 @@
"vulnerability": ["elderly", "health_issues", "limited_mobility"],
"vulnerability_score": 70, # CEL: Elderly couple vulnerability
"has_formal_pension": False, # CEL: Elderly pension eligibility
- "district": "Central District",
+ "area_ref": "spp_demo.area_phl_calamba",
+ "area_kind": "municipality",
},
"journey": [
{"action": "register_household", "days_back": 250},
@@ -676,41 +537,13 @@
"cel_check": "age_retirement",
"days_back": 232,
},
- {
- "action": "enroll_program",
- "program": "Elderly Pension",
- "days_back": 230,
- },
- {
- "action": "enroll_program",
- "program": "Food Assistance",
- "days_back": 220,
- },
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 200,
- },
+ {"action": "enroll_program", "program": "Elderly Pension", "days_back": 230},
+ {"action": "enroll_program", "program": "Food Assistance", "days_back": 220},
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 200},
{"action": "create_in_kind", "item": "Food Basket", "days_back": 195},
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 140,
- },
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 80,
- },
- {
- "action": "create_payment",
- "amount": 200,
- "status": "paid",
- "days_back": 20,
- },
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 140},
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 80},
+ {"action": "create_payment", "amount": 200, "status": "paid", "days_back": 20},
],
"demo_points": [
"Small elderly household",
@@ -728,12 +561,7 @@
"profile": {
"head": {"name": "James Nguyen", "gender": "male", "age": 52},
"adults": [
- {
- "name": "Linda Nguyen",
- "gender": "female",
- "age": 48,
- "relation": "spouse",
- },
+ {"name": "Linda Nguyen", "gender": "female", "age": 48, "relation": "spouse"},
{
"name": "Michael Nguyen",
"gender": "male",
@@ -741,17 +569,13 @@
"relation": "brother",
"disability_status": "disabled",
},
- {
- "name": "Sarah Nguyen",
- "gender": "female",
- "age": 44,
- "relation": "sister-in-law",
- },
+ {"name": "Sarah Nguyen", "gender": "female", "age": 44, "relation": "sister-in-law"},
],
"farm_size": 6.0,
"farm_size_hectares": 6.0, # CEL: Extended family farm
"farm_type": "mixed",
- "district": "Southern District",
+ "area_ref": "spp_demo.area_phl_santa_rosa",
+ "area_kind": "municipality",
"vulnerability": ["disability"],
"vulnerability_score": 65, # CEL: Disability in household
"disabled_count": 1, # CEL: Member with disability
@@ -761,35 +585,11 @@
{"action": "register_household", "days_back": 300},
{"action": "add_household_members", "days_back": 295},
{"action": "vulnerability_assessment", "score": "medium", "days_back": 290},
- {
- "action": "enroll_program",
- "program": "Cash Transfer Program",
- "days_back": 280,
- },
- {
- "action": "create_payment",
- "amount": 400,
- "status": "paid",
- "days_back": 250,
- },
- {
- "action": "create_payment",
- "amount": 400,
- "status": "paid",
- "days_back": 190,
- },
- {
- "action": "create_payment",
- "amount": 400,
- "status": "paid",
- "days_back": 130,
- },
- {
- "action": "create_payment",
- "amount": 400,
- "status": "paid",
- "days_back": 70,
- },
+ {"action": "enroll_program", "program": "Cash Transfer Program", "days_back": 280},
+ {"action": "create_payment", "amount": 400, "status": "paid", "days_back": 250},
+ {"action": "create_payment", "amount": 400, "status": "paid", "days_back": 190},
+ {"action": "create_payment", "amount": 400, "status": "paid", "days_back": 130},
+ {"action": "create_payment", "amount": 400, "status": "paid", "days_back": 70},
],
"demo_points": [
"Extended family structure",
@@ -818,34 +618,16 @@
},
"journey": [
{"action": "emergency_register", "days_back": 60},
- {
- "action": "vulnerability_assessment",
- "score": "very_high",
- "days_back": 58,
- },
+ {"action": "vulnerability_assessment", "score": "very_high", "days_back": 58},
{
"action": "verify_eligibility",
"program": "Emergency Cash Transfer",
"cel_check": "vulnerability_metric",
"days_back": 56,
},
- {
- "action": "enroll_program",
- "program": "Emergency Cash Transfer",
- "days_back": 55,
- },
- {
- "action": "create_payment",
- "amount": 500,
- "status": "paid",
- "days_back": 50,
- },
- {
- "action": "create_payment",
- "amount": 500,
- "status": "paid",
- "days_back": 35,
- },
+ {"action": "enroll_program", "program": "Emergency Cash Transfer", "days_back": 55},
+ {"action": "create_payment", "amount": 500, "status": "paid", "days_back": 50},
+ {"action": "create_payment", "amount": 500, "status": "paid", "days_back": 35},
{
"action": "create_grm_ticket",
"title": "Request for resettlement support",
@@ -883,11 +665,7 @@
"ticket_type": "inquiry",
"days_back": 45,
},
- {
- "action": "respond_ticket",
- "response": "Program information provided",
- "days_back": 43,
- },
+ {"action": "respond_ticket", "response": "Program information provided", "days_back": 43},
{"action": "apply_program", "program": "Food Assistance", "days_back": 40},
{"action": "enroll_program", "program": "Food Assistance", "days_back": 30},
],
@@ -906,56 +684,29 @@
"head": {"name": "David Martinez", "gender": "male", "age": 48},
"spouse": {"name": "Sofia Martinez", "gender": "female", "age": 45},
"children": [
- {
- "name": "Miguel Martinez",
- "gender": "male",
- "age": 12,
- "disability_status": "disabled",
- },
+ {"name": "Miguel Martinez", "gender": "male", "age": 12, "disability_status": "disabled"},
],
"farm_size": 1.5,
"farm_size_hectares": 1.5, # CEL: Small farm household
"disabled_count": 1, # CEL: Disability Support Grant eligibility
"child_count": 1,
- "district": "Western District",
+ "area_ref": "spp_demo.area_phl_makati",
+ "area_kind": "municipality",
},
"journey": [
{"action": "register_household", "days_back": 120},
{"action": "add_household_members", "days_back": 115},
- {
- "action": "disability_assessment",
- "member": "Miguel Martinez",
- "days_back": 110,
- },
+ {"action": "disability_assessment", "member": "Miguel Martinez", "days_back": 110},
{
"action": "verify_eligibility",
"program": "Disability Support Grant",
"cel_check": "member_exists_disabled",
"days_back": 102,
},
- {
- "action": "enroll_program",
- "program": "Disability Support Grant",
- "days_back": 100,
- },
- {
- "action": "create_payment",
- "amount": 175,
- "status": "paid",
- "days_back": 90,
- },
- {
- "action": "create_payment",
- "amount": 175,
- "status": "paid",
- "days_back": 60,
- },
- {
- "action": "create_payment",
- "amount": 175,
- "status": "paid",
- "days_back": 30,
- },
+ {"action": "enroll_program", "program": "Disability Support Grant", "days_back": 100},
+ {"action": "create_payment", "amount": 175, "status": "paid", "days_back": 90},
+ {"action": "create_payment", "amount": 175, "status": "paid", "days_back": 60},
+ {"action": "create_payment", "amount": 175, "status": "paid", "days_back": 30},
],
"demo_points": [
"Household with disabled member",
@@ -977,12 +728,7 @@
"profile": {"gender": "male", "age": 40, "farm_size": 1.5},
"journey": [
{"action": "register", "days_back": 30},
- {
- "action": "apply_program",
- "program": "Input Subsidy Program",
- "status": "pending",
- "days_back": 25,
- },
+ {"action": "apply_program", "program": "Input Subsidy Program", "status": "pending", "days_back": 25},
],
},
{
@@ -1012,11 +758,7 @@
"profile": {"gender": "male", "age": 45, "farm_size": 2.0},
"journey": [
{"action": "register", "days_back": 200},
- {
- "action": "enroll_program",
- "program": "Cash Transfer Program",
- "days_back": 180,
- },
+ {"action": "enroll_program", "program": "Cash Transfer Program", "days_back": 180},
{"action": "create_grm_ticket", "title": "Ticket 1", "days_back": 150},
{"action": "create_grm_ticket", "title": "Ticket 2", "days_back": 100},
{"action": "create_grm_ticket", "title": "Ticket 3", "days_back": 50},
@@ -1039,12 +781,7 @@
"type": "farmer",
"story_title": "Contract Farmer",
"story_description": "Shows guaranteed market arrangement",
- "profile": {
- "gender": "male",
- "age": 48,
- "farm_size": 4.0,
- "contract_farming": True,
- },
+ "profile": {"gender": "male", "age": 48, "farm_size": 4.0, "contract_farming": True},
"journey": [
{"action": "register", "days_back": 300},
{"action": "add_farm_details", "days_back": 295},
@@ -1066,12 +803,7 @@
"story_title": "Tutorial: Not Eligible (High Income)",
"story_description": "Tutorial household with income above threshold - NOT ELIGIBLE",
"profile": {
- "head": {
- "name": "Roberto Garcia",
- "gender": "male",
- "age": 42,
- "income": 15000,
- },
+ "head": {"name": "Roberto Garcia", "gender": "male", "age": 42, "income": 15000},
"spouse": {"name": "Maria Garcia", "gender": "female", "age": 38},
"children": [
{"name": "Carlos Garcia", "gender": "male", "age": 12},
@@ -1096,19 +828,10 @@
"story_title": "Tutorial: Eligible (Low Income + Child Under 5)",
"story_description": "Tutorial household meeting both criteria - ELIGIBLE",
"profile": {
- "head": {
- "name": "Jose Santos",
- "gender": "male",
- "age": 35,
- "income": 8000,
- },
+ "head": {"name": "Jose Santos", "gender": "male", "age": 35, "income": 8000},
"spouse": {"name": "Ana Santos", "gender": "female", "age": 32},
"children": [
- {
- "name": "Mia Santos",
- "gender": "female",
- "age": 4,
- }, # Born ~2021, under 5
+ {"name": "Mia Santos", "gender": "female", "age": 4}, # Born ~2021, under 5
],
"child_count": 1,
"district": "Northern District",
@@ -1129,12 +852,7 @@
"story_title": "Tutorial: Not Eligible (Income Above Threshold)",
"story_description": "Tutorial household with income above threshold - NOT ELIGIBLE",
"profile": {
- "head": {
- "name": "Pedro Cruz",
- "gender": "male",
- "age": 45,
- "income": 12000,
- },
+ "head": {"name": "Pedro Cruz", "gender": "male", "age": 45, "income": 12000},
"spouse": {"name": "Teresa Cruz", "gender": "female", "age": 42},
"children": [
{"name": "Juan Cruz", "gender": "male", "age": 15},
@@ -1160,19 +878,10 @@
"story_title": "Tutorial: Eligible (Low Income + Child Under 5)",
"story_description": "Tutorial household meeting both criteria - ELIGIBLE",
"profile": {
- "head": {
- "name": "Ramon Reyes",
- "gender": "male",
- "age": 30,
- "income": 6000,
- },
+ "head": {"name": "Ramon Reyes", "gender": "male", "age": 30, "income": 6000},
"spouse": {"name": "Elena Reyes", "gender": "female", "age": 28},
"children": [
- {
- "name": "Lucia Reyes",
- "gender": "female",
- "age": 2,
- }, # Born ~2023, under 5
+ {"name": "Lucia Reyes", "gender": "female", "age": 2}, # Born ~2023, under 5
],
"child_count": 1,
"district": "Southern District",
@@ -1193,12 +902,7 @@
"story_title": "Tutorial: Not Eligible (High Income)",
"story_description": "Tutorial household with highest income - NOT ELIGIBLE",
"profile": {
- "head": {
- "name": "Antonio Ramos",
- "gender": "male",
- "age": 48,
- "income": 18000,
- },
+ "head": {"name": "Antonio Ramos", "gender": "male", "age": 48, "income": 18000},
"spouse": {"name": "Rosa Ramos", "gender": "female", "age": 45},
"children": [
{"name": "Diego Ramos", "gender": "male", "age": 18},
diff --git a/spp_demo/security/ir.model.access.csv b/spp_demo/security/ir.model.access.csv
index 70643210..43e57ca9 100644
--- a/spp_demo/security/ir.model.access.csv
+++ b/spp_demo/security/ir.model.access.csv
@@ -24,3 +24,8 @@ access_spp_demo_data_id_types_create,Demo ID Types Create Access,spp_demo.model_
access_spp_demo_data_bank_types_create,Demo Bank Types Create Access,spp_demo.model_spp_demo_data_bank_types,spp_registry.group_registry_create,1,0,1,1
access_spp_apps_wizard_create,Apps Wizard Create Access,spp_demo.model_spp_apps_wizard,spp_registry.group_registry_create,1,0,1,0
access_spp_missing_module_create,Apps Missing Modules Create Access,spp_demo.model_spp_missing_module,spp_registry.group_registry_create,1,0,1,0
+
+access_spp_demo_area_loader_admin,Demo Area Loader Admin Access,spp_demo.model_spp_demo_area_loader,base.group_system,1,1,1,1
+access_spp_demo_area_loader_read,Demo Area Loader Read Access,spp_demo.model_spp_demo_area_loader,spp_registry.group_registry_read,1,0,0,0
+access_spp_demo_area_loader_write,Demo Area Loader Write Access,spp_demo.model_spp_demo_area_loader,spp_registry.group_registry_write,1,1,0,0
+access_spp_demo_area_loader_create,Demo Area Loader Create Access,spp_demo.model_spp_demo_area_loader,spp_registry.group_registry_create,1,0,1,0
diff --git a/spp_demo/tests/__init__.py b/spp_demo/tests/__init__.py
index 954a1827..52c240bb 100644
--- a/spp_demo/tests/__init__.py
+++ b/spp_demo/tests/__init__.py
@@ -5,3 +5,4 @@
from . import test_res_partner
from . import test_apps_wizard
from . import test_demo_stories
+from . import test_demo_area_loader
diff --git a/spp_demo/tests/test_demo_area_loader.py b/spp_demo/tests/test_demo_area_loader.py
new file mode 100644
index 00000000..585c2b0b
--- /dev/null
+++ b/spp_demo/tests/test_demo_area_loader.py
@@ -0,0 +1,238 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+"""Tests for the Demo Area Loader functionality."""
+
+import logging
+
+from odoo.tests.common import TransactionCase
+
+_logger = logging.getLogger(__name__)
+
+
+class TestDemoAreaLoader(TransactionCase):
+ """Test cases for spp.demo.area.loader model."""
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.loader_model = cls.env["spp.demo.area.loader"]
+ cls.area_model = cls.env["spp.area"]
+ cls.area_kind_model = cls.env["spp.area.type"]
+
+ def test_supported_countries(self):
+ """Test that supported countries are defined."""
+ supported = self.loader_model.SUPPORTED_COUNTRIES
+ self.assertIsInstance(supported, list)
+ self.assertGreater(len(supported), 0)
+
+ # Check Philippines is supported
+ country_codes = [code for code, name in supported]
+ self.assertIn("phl", country_codes)
+ self.assertIn("lka", country_codes)
+ self.assertIn("tgo", country_codes)
+
+ def test_get_country_name(self):
+ """Test country name lookup."""
+ name = self.loader_model._get_country_name("phl")
+ self.assertEqual(name, "Philippines")
+
+ name = self.loader_model._get_country_name("lka")
+ self.assertEqual(name, "Sri Lanka")
+
+ name = self.loader_model._get_country_name("tgo")
+ self.assertEqual(name, "Togo")
+
+ # Unknown country returns uppercase code
+ name = self.loader_model._get_country_name("xyz")
+ self.assertEqual(name, "XYZ")
+
+ def test_is_gis_installed(self):
+ """Test GIS module detection."""
+ # This is a basic test - actual result depends on installed modules
+ result = self.loader_model._is_gis_installed()
+ self.assertIsInstance(result, bool)
+
+ def test_load_phl_areas(self):
+ """Test loading Philippines area data."""
+ # Count areas before
+ initial_count = self.area_model.search_count([])
+
+ # Load Philippines areas
+ loader = self.loader_model.create(
+ {
+ "country_code": "phl",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("phl")
+
+ # Verify areas were created
+ final_count = self.area_model.search_count([])
+ self.assertGreater(final_count, initial_count, "Areas should be created for Philippines")
+
+ # Verify specific areas exist
+ ncr = self.area_model.search([("code", "=", "PH-00")], limit=1)
+ self.assertTrue(ncr, "NCR region should exist")
+
+ quezon_city = self.area_model.search([("code", "=", "PH-00-74")], limit=1)
+ self.assertTrue(quezon_city, "Quezon City should exist")
+
+ # Verify area hierarchy
+ metro_manila = self.area_model.search([("code", "=", "PH-00-MM")], limit=1)
+ if metro_manila and ncr:
+ self.assertEqual(metro_manila.parent_id.id, ncr.id, "Metro Manila should be child of NCR")
+
+ def test_load_lka_areas(self):
+ """Test loading Sri Lanka area data."""
+ # Load Sri Lanka areas
+ loader = self.loader_model.create(
+ {
+ "country_code": "lka",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("lka")
+
+ # Verify specific areas exist
+ western = self.area_model.search([("code", "=", "LK-1")], limit=1)
+ self.assertTrue(western, "Western Province should exist")
+
+ colombo = self.area_model.search([("code", "=", "LK-11")], limit=1)
+ self.assertTrue(colombo, "Colombo District should exist")
+
+ def test_load_tgo_areas(self):
+ """Test loading Togo area data."""
+ # Load Togo areas
+ loader = self.loader_model.create(
+ {
+ "country_code": "tgo",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("tgo")
+
+ # Verify specific areas exist
+ maritime = self.area_model.search([("code", "=", "TG-M")], limit=1)
+ self.assertTrue(maritime, "Maritime region should exist")
+
+ lome = self.area_model.search([("code", "=", "TG-M-GOL-LOM")], limit=1)
+ self.assertTrue(lome, "Lome canton should exist")
+
+ def test_load_country_areas_programmatic(self):
+ """Test programmatic area loading method."""
+ result = self.loader_model.load_country_areas("phl", load_shapes=False)
+
+ self.assertIn("country_code", result)
+ self.assertEqual(result["country_code"], "phl")
+ self.assertIn("shapes_loaded", result)
+ self.assertIn("gis_available", result)
+
+ def test_geojson_to_wkt_polygon(self):
+ """Test GeoJSON polygon to WKT conversion."""
+ loader = self.loader_model.create(
+ {
+ "country_code": "phl",
+ "load_shapes": False,
+ }
+ )
+
+ geometry = {
+ "type": "Polygon",
+ "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
+ }
+
+ wkt = loader._geojson_to_wkt(geometry)
+ self.assertIsNotNone(wkt)
+ self.assertTrue(wkt.startswith("POLYGON"))
+
+ def test_geojson_to_wkt_invalid(self):
+ """Test GeoJSON conversion with invalid input."""
+ loader = self.loader_model.create(
+ {
+ "country_code": "phl",
+ "load_shapes": False,
+ }
+ )
+
+ # Empty geometry
+ wkt = loader._geojson_to_wkt({})
+ self.assertIsNone(wkt)
+
+ # Missing coordinates
+ wkt = loader._geojson_to_wkt({"type": "Polygon"})
+ self.assertIsNone(wkt)
+
+
+class TestDemoAreaLoaderAreaKinds(TransactionCase):
+ """Test area kinds creation for each country."""
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.loader_model = cls.env["spp.demo.area.loader"]
+ cls.area_kind_model = cls.env["spp.area.type"]
+
+ def test_phl_area_kinds(self):
+ """Test Philippines area kinds hierarchy."""
+ loader = self.loader_model.create(
+ {
+ "country_code": "phl",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("phl")
+
+ # Check region kind exists
+ region = self.area_kind_model.search([("name", "=", "Region")], limit=1)
+ self.assertTrue(region, "Region area kind should exist")
+
+ # Check province kind exists and has region as parent
+ province = self.area_kind_model.search([("name", "=", "Province")], limit=1)
+ self.assertTrue(province, "Province area kind should exist")
+
+ # Check municipality kind exists
+ municipality = self.area_kind_model.search([("name", "=", "City/Municipality")], limit=1)
+ self.assertTrue(municipality, "City/Municipality area kind should exist")
+
+ # Check barangay kind exists
+ barangay = self.area_kind_model.search([("name", "=", "Barangay")], limit=1)
+ self.assertTrue(barangay, "Barangay area kind should exist")
+
+ def test_lka_area_kinds(self):
+ """Test Sri Lanka area kinds hierarchy."""
+ loader = self.loader_model.create(
+ {
+ "country_code": "lka",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("lka")
+
+ # Check DS Division kind exists
+ ds_division = self.area_kind_model.search([("name", "=", "DS Division")], limit=1)
+ self.assertTrue(ds_division, "DS Division area kind should exist")
+
+ # Check GN Division kind exists
+ gn_division = self.area_kind_model.search([("name", "=", "GN Division")], limit=1)
+ self.assertTrue(gn_division, "GN Division area kind should exist")
+
+ def test_tgo_area_kinds(self):
+ """Test Togo area kinds hierarchy."""
+ loader = self.loader_model.create(
+ {
+ "country_code": "tgo",
+ "load_shapes": False,
+ }
+ )
+ loader._load_xml_data("tgo")
+
+ # Check prefecture kind exists
+ prefecture = self.area_kind_model.search([("name", "=", "Prefecture")], limit=1)
+ self.assertTrue(prefecture, "Prefecture area kind should exist")
+
+ # Check canton kind exists
+ canton = self.area_kind_model.search([("name", "=", "Canton")], limit=1)
+ self.assertTrue(canton, "Canton area kind should exist")
+
+ # Check village kind exists
+ village = self.area_kind_model.search([("name", "=", "Village")], limit=1)
+ self.assertTrue(village, "Village area kind should exist")
diff --git a/spp_demo/tests/test_demo_data_generator.py b/spp_demo/tests/test_demo_data_generator.py
index 9e5dc8c2..5289f9d8 100644
--- a/spp_demo/tests/test_demo_data_generator.py
+++ b/spp_demo/tests/test_demo_data_generator.py
@@ -612,3 +612,145 @@ def test_27_edge_case_regex_generation_complex(self):
result2 = generator.generate_id_from_regex(pattern2)
self.assertIsNotNone(result2)
self.assertEqual(len(result2), 5)
+
+ def test_28_group_gets_area_id(self):
+ """Test that groups are assigned an area_id when areas exist."""
+ # Create area hierarchy
+ area_type_region = self.env["spp.area.type"].create({"name": "Test Region"})
+ area_type_barangay = self.env["spp.area.type"].create(
+ {"name": "Test Barangay", "parent_id": area_type_region.id}
+ )
+
+ region = self.env["spp.area"].create(
+ {
+ "draft_name": "Test Region",
+ "code": "TEST-REG",
+ "area_type_id": area_type_region.id,
+ }
+ )
+ self.env["spp.area"].create(
+ {
+ "draft_name": "Test Barangay",
+ "code": "TEST-BRG",
+ "area_type_id": area_type_barangay.id,
+ "parent_id": region.id,
+ }
+ )
+
+ generator = self.env["spp.demo.data.generator"].create(
+ {
+ "name": "Area Assignment Test",
+ "locale_origin": self.test_country.id,
+ }
+ )
+
+ from faker import Faker
+
+ fake = Faker("en_US")
+ group_vals = generator.get_group_vals(fake)
+
+ # Group should have area_id assigned
+ self.assertIn("area_id", group_vals)
+ self.assertTrue(group_vals["area_id"])
+
+ def test_29_group_prefers_leaf_areas(self):
+ """Test that groups are assigned to leaf-level areas (not regions)."""
+ # Create a hierarchy: region -> province -> barangay
+ area_type_region = self.env["spp.area.type"].create({"name": "Test Region L"})
+ area_type_province = self.env["spp.area.type"].create(
+ {"name": "Test Province L", "parent_id": area_type_region.id}
+ )
+ area_type_leaf = self.env["spp.area.type"].create({"name": "Test Leaf L", "parent_id": area_type_province.id})
+
+ region = self.env["spp.area"].create(
+ {
+ "draft_name": "Region Parent",
+ "code": "TEST-RP",
+ "area_type_id": area_type_region.id,
+ }
+ )
+ province = self.env["spp.area"].create(
+ {
+ "draft_name": "Province Mid",
+ "code": "TEST-PM",
+ "area_type_id": area_type_province.id,
+ "parent_id": region.id,
+ }
+ )
+ leaf1 = self.env["spp.area"].create(
+ {
+ "draft_name": "Leaf One",
+ "code": "TEST-L1",
+ "area_type_id": area_type_leaf.id,
+ "parent_id": province.id,
+ }
+ )
+ leaf2 = self.env["spp.area"].create(
+ {
+ "draft_name": "Leaf Two",
+ "code": "TEST-L2",
+ "area_type_id": area_type_leaf.id,
+ "parent_id": province.id,
+ }
+ )
+
+ generator = self.env["spp.demo.data.generator"].create(
+ {
+ "name": "Leaf Area Test",
+ "locale_origin": self.test_country.id,
+ }
+ )
+
+ from faker import Faker
+
+ fake = Faker("en_US")
+
+ # Generate multiple groups and verify they all get leaf areas
+ leaf_ids = {leaf1.id, leaf2.id}
+ for _ in range(10):
+ group_vals = generator.get_group_vals(fake)
+ self.assertIn(
+ group_vals["area_id"],
+ leaf_ids,
+ "Group should be assigned to a leaf-level area, not a parent area",
+ )
+
+ def test_30_members_inherit_group_area(self):
+ """Test that individual members inherit their group's area_id."""
+ area_type = self.env["spp.area.type"].create({"name": "Test Area MIA"})
+ self.env["spp.area"].create(
+ {
+ "draft_name": "Member Inherit Area",
+ "code": "TEST-MIA",
+ "area_type_id": area_type.id,
+ }
+ )
+
+ generator = self.env["spp.demo.data.generator"].create(
+ {
+ "name": "Member Inherit Test",
+ "number_of_groups": 1,
+ "members_range_from": 3,
+ "members_range_to": 3,
+ "locale_origin": self.test_country.id,
+ }
+ )
+
+ from faker import Faker
+
+ fake = Faker("en_US")
+ generator._generate_demo_data(fake)
+
+ # Find the created group
+ group = self.env["res.partner"].search([("demo_data_group_generator_id", "=", generator.id)], limit=1)
+ self.assertTrue(group.area_id, "Group should have an area_id")
+
+ # All members should share the group's area
+ members = self.env["res.partner"].search([("demo_data_individual_generator_id", "=", generator.id)])
+ self.assertTrue(len(members) >= 3)
+ for member in members:
+ self.assertEqual(
+ member.area_id,
+ group.area_id,
+ f"Member '{member.name}' should inherit group's area_id",
+ )
diff --git a/spp_demo/tests/test_demo_stories.py b/spp_demo/tests/test_demo_stories.py
index 0d8e41d9..caafee49 100644
--- a/spp_demo/tests/test_demo_stories.py
+++ b/spp_demo/tests/test_demo_stories.py
@@ -60,10 +60,7 @@ def test_02_get_all_stories(self):
background_stories = demo_stories.get_background_stories()
tutorial_stories = demo_stories.get_tutorial_stories()
- self.assertEqual(
- len(all_stories),
- len(main_stories) + len(background_stories) + len(tutorial_stories),
- )
+ self.assertEqual(len(all_stories), len(main_stories) + len(background_stories) + len(tutorial_stories))
self.assertGreater(len(main_stories), 0)
def test_03_get_story_by_id(self):
diff --git a/spp_demo/tests/test_res_config_settings.py b/spp_demo/tests/test_res_config_settings.py
index 755fa080..399da3b3 100644
--- a/spp_demo/tests/test_res_config_settings.py
+++ b/spp_demo/tests/test_res_config_settings.py
@@ -131,26 +131,11 @@ def test_08_set_multiple_values(self):
config.execute()
# Verify all parameters were set
- self.assertEqual(
- int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.number_of_groups")),
- 100,
- )
- self.assertEqual(
- int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.members_range_from")),
- 2,
- )
- self.assertEqual(
- int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.members_range_to")),
- 12,
- )
- self.assertEqual(
- int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.batch_size")),
- 300,
- )
- self.assertEqual(
- int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.queue_job_minimum_size")),
- 2000,
- )
+ self.assertEqual(int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.number_of_groups")), 100)
+ self.assertEqual(int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.members_range_from")), 2)
+ self.assertEqual(int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.members_range_to")), 12)
+ self.assertEqual(int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.batch_size")), 300)
+ self.assertEqual(int(self.env["ir.config_parameter"].sudo().get_param("spp_demo.queue_job_minimum_size")), 2000)
def test_09_default_values(self):
"""Test default values when no config parameters are set"""
diff --git a/spp_demo/tests/test_res_country.py b/spp_demo/tests/test_res_country.py
index 292f4eae..ea8ade2f 100644
--- a/spp_demo/tests/test_res_country.py
+++ b/spp_demo/tests/test_res_country.py
@@ -209,14 +209,7 @@ def test_10_inherit_model_check(self):
self.assertIn("code", country._fields)
# Verify our custom fields are there
- custom_fields = [
- "lat_min",
- "lat_max",
- "lon_min",
- "lon_max",
- "faker_locale",
- "is_faker_locale_available",
- ]
+ custom_fields = ["lat_min", "lat_max", "lon_min", "lon_max", "faker_locale", "is_faker_locale_available"]
for field in custom_fields:
self.assertIn(field, country._fields)
diff --git a/spp_demo/tests/test_res_partner.py b/spp_demo/tests/test_res_partner.py
index e59978ed..570c3f3f 100644
--- a/spp_demo/tests/test_res_partner.py
+++ b/spp_demo/tests/test_res_partner.py
@@ -240,11 +240,7 @@ def test_12_inherit_model_check(self):
self.assertIn("email", partner._fields)
# Verify our custom fields are there
- custom_fields = [
- "demo_data_group_generator_id",
- "demo_data_individual_generator_id",
- "gps_coordinates",
- ]
+ custom_fields = ["demo_data_group_generator_id", "demo_data_individual_generator_id", "gps_coordinates"]
for field in custom_fields:
self.assertIn(field, partner._fields)
diff --git a/spp_demo/views/demo_data_generator_view.xml b/spp_demo/views/demo_data_generator_view.xml
index 94aafcc1..10584e8f 100644
--- a/spp_demo/views/demo_data_generator_view.xml
+++ b/spp_demo/views/demo_data_generator_view.xml
@@ -250,6 +250,41 @@
+
+
+
+
+
+ spp.demo.area.loader.form
+ spp.demo.area.loader
+
+
+
+
+
+
+
+ Load Geographic Data
+ spp.demo.area.loader
+ form
+ new
+ {}
+
+
+
+
+