diff --git a/farm_field_geo/README.rst b/farm_field_geo/README.rst new file mode 100644 index 0000000..34c66cf --- /dev/null +++ b/farm_field_geo/README.rst @@ -0,0 +1,120 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +Farm Field — Geospatial +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:156d6ff91db3f7522e9501d6a413cbc2a2e29881632c0f19543ed5de002262fc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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-ledoent%2Ffarm--pack-lightgray.png?logo=github + :target: https://github.com/ledoent/farm-pack/tree/19.0/farm_field_geo + :alt: ledoent/farm-pack + +|badge1| |badge2| |badge3| + +Adds PostGIS polygon boundaries to ``farm.field`` and auto-computes +acreage from the geometry. + +Without this module, ``farm.field.acres`` is a manual decimal entry. +With it, set a WGS84 polygon as the field's boundary and the acreage +updates from the polygon area, reprojected to EPSG:5070 (Conus Albers +Equal Area) — the projection NRCS and NASS use for lower-48 area +calculations, so the number matches what soil-survey and yield tooling +would report for the same parcel. + +``acres`` remains editable as a fallback: fields without a digitized +boundary keep working with a manual estimate, and the compute only fires +once a polygon is set. + +**Scope.** This module ships a single ``GeoPolygon`` per field — +non-contiguous plots (two disconnected polygons that are +administratively one "field") should be recorded as separate +``farm.field`` records for now. A future ``farm_field_multi_polygon`` +extension can promote the column to ``GeoMultiPolygon`` if the demand +materializes. + +**Install requirement.** The Postgres instance must have PostGIS +enabled. ``base_geoengine`` declares the extension; install will fail +cleanly if the extension is missing. The base ``farm_field`` module +stays usable without PostGIS — install this extension only when geometry +support is wanted. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Open Farm → Fields → Fields and pick a field. +2. The **Boundary** notebook tab renders a Leaflet map of the field's + polygon. +3. Set the polygon via the geoengine map view (vertex drag works there) + or by importing a WKT string. In-form vertex editing through the + boundary tab arrives once ``web_leaflet_draw_lib`` is migrated to + Odoo 19. +4. Save. The **Acres** field auto-recomputes from the polygon area, + reprojected to EPSG:5070 Albers Equal Area. +5. Switch the action's view to **Map** (geoengine) to see every field at + once. + +The ``acres`` field stays editable as a fallback: fields without a +digitized boundary can carry a manual estimate. Once a polygon is set, +the compute overrides the manual value. + +Multi-plot fields (a "north 40" that is two non-contiguous polygons) +need ``GeoMultiPolygon`` rather than ``GeoPolygon`` — out of scope for +the MVP; record each plot as its own ``farm.field`` for now. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Ledo Enterprises + +Contributors +------------ + +- Daniel Kendall + +Maintainers +----------- + +.. |maintainer-dnplkndll| image:: https://github.com/dnplkndll.png?size=40px + :target: https://github.com/dnplkndll + :alt: dnplkndll + +Current maintainer: + +|maintainer-dnplkndll| + +This module is part of the `ledoent/farm-pack `_ project on GitHub. + +You are welcome to contribute. diff --git a/farm_field_geo/__init__.py b/farm_field_geo/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/farm_field_geo/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/farm_field_geo/__manifest__.py b/farm_field_geo/__manifest__.py new file mode 100644 index 0000000..ad41125 --- /dev/null +++ b/farm_field_geo/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Farm Field — Geospatial", + "version": "19.0.1.0.0", + "summary": "PostGIS polygons + auto-computed acreage for farm.field", + "author": "Ledo Enterprises, Odoo Community Association (OCA)", + "maintainers": ["dnplkndll"], + "website": "https://github.com/ledoent/farm-pack", + "license": "AGPL-3", + "category": "Vertical/Agriculture", + "depends": [ + "farm_field", + "base_geoengine", + ], + "data": [ + "views/farm_field_views.xml", + ], + "installable": True, + "application": False, +} diff --git a/farm_field_geo/models/__init__.py b/farm_field_geo/models/__init__.py new file mode 100644 index 0000000..ec95450 --- /dev/null +++ b/farm_field_geo/models/__init__.py @@ -0,0 +1 @@ +from . import farm_field diff --git a/farm_field_geo/models/farm_field.py b/farm_field_geo/models/farm_field.py new file mode 100644 index 0000000..6bac78c --- /dev/null +++ b/farm_field_geo/models/farm_field.py @@ -0,0 +1,37 @@ +from odoo import api, fields, models + +# Square meters in one US survey acre. Used to convert EPSG:5070 area +# (Albers Equal Area, units = m^2) to acres. +M2_PER_ACRE = 4046.8564224 + +# Conus Albers Equal Area — the projection NRCS and USDA use for area +# calculations across the lower 48. We reproject the WGS84 polygon to 5070 +# before measuring area so the number matches what soil-survey and NASS +# tooling would report for the same parcel. +ALBERS_CONUS_SRID = 5070 + + +class FarmField(models.Model): + _inherit = "farm.field" + + geom = fields.GeoPolygon( + string="Boundary", + srid=4326, + help="Field boundary as a WGS84 polygon. Drives the computed acreage; " + "stored as PostGIS geometry.", + ) + acres = fields.Float( + compute="_compute_acres_from_geom", + store=True, + readonly=False, + digits=(8, 2), + help="Auto-computed from the polygon area when a boundary is drawn. " + "Editable as a fallback for fields without a digitized boundary yet.", + ) + + @api.depends("geom") + def _compute_acres_from_geom(self): + for rec in self: + if not rec.geom: + continue + rec.acres = rec.geom.transform(ALBERS_CONUS_SRID).area / M2_PER_ACRE diff --git a/farm_field_geo/pyproject.toml b/farm_field_geo/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/farm_field_geo/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/farm_field_geo/readme/CONTRIBUTORS.md b/farm_field_geo/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..c71b705 --- /dev/null +++ b/farm_field_geo/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Daniel Kendall <dkendall@ledoweb.com> diff --git a/farm_field_geo/readme/DESCRIPTION.md b/farm_field_geo/readme/DESCRIPTION.md new file mode 100644 index 0000000..2f68ede --- /dev/null +++ b/farm_field_geo/readme/DESCRIPTION.md @@ -0,0 +1,23 @@ +Adds PostGIS polygon boundaries to `farm.field` and auto-computes acreage +from the geometry. + +Without this module, `farm.field.acres` is a manual decimal entry. With it, +set a WGS84 polygon as the field's boundary and the acreage updates from the +polygon area, reprojected to EPSG:5070 (Conus Albers Equal Area) — the +projection NRCS and NASS use for lower-48 area calculations, so the number +matches what soil-survey and yield tooling would report for the same parcel. + +`acres` remains editable as a fallback: fields without a digitized boundary +keep working with a manual estimate, and the compute only fires once a +polygon is set. + +**Scope.** This module ships a single `GeoPolygon` per field — non-contiguous +plots (two disconnected polygons that are administratively one "field") +should be recorded as separate `farm.field` records for now. A future +`farm_field_multi_polygon` extension can promote the column to +`GeoMultiPolygon` if the demand materializes. + +**Install requirement.** The Postgres instance must have PostGIS enabled. +`base_geoengine` declares the extension; install will fail cleanly if the +extension is missing. The base `farm_field` module stays usable without +PostGIS — install this extension only when geometry support is wanted. diff --git a/farm_field_geo/readme/USAGE.md b/farm_field_geo/readme/USAGE.md new file mode 100644 index 0000000..61bd247 --- /dev/null +++ b/farm_field_geo/readme/USAGE.md @@ -0,0 +1,16 @@ +1. Open Farm → Fields → Fields and pick a field. +2. The **Boundary** notebook tab renders a Leaflet map of the field's polygon. +3. Set the polygon via the geoengine map view (vertex drag works there) or by + importing a WKT string. In-form vertex editing through the boundary tab + arrives once `web_leaflet_draw_lib` is migrated to Odoo 19. +4. Save. The **Acres** field auto-recomputes from the polygon area, reprojected + to EPSG:5070 Albers Equal Area. +5. Switch the action's view to **Map** (geoengine) to see every field at once. + +The `acres` field stays editable as a fallback: fields without a digitized +boundary can carry a manual estimate. Once a polygon is set, the compute +overrides the manual value. + +Multi-plot fields (a "north 40" that is two non-contiguous polygons) need +`GeoMultiPolygon` rather than `GeoPolygon` — out of scope for the MVP; +record each plot as its own `farm.field` for now. diff --git a/farm_field_geo/readme/newsfragments/.gitkeep b/farm_field_geo/readme/newsfragments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/farm_field_geo/static/description/index.html b/farm_field_geo/static/description/index.html new file mode 100644 index 0000000..f604459 --- /dev/null +++ b/farm_field_geo/static/description/index.html @@ -0,0 +1,468 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Farm Field — Geospatial

+ +

Beta License: AGPL-3 ledoent/farm-pack

+

Adds PostGIS polygon boundaries to farm.field and auto-computes +acreage from the geometry.

+

Without this module, farm.field.acres is a manual decimal entry. +With it, set a WGS84 polygon as the field’s boundary and the acreage +updates from the polygon area, reprojected to EPSG:5070 (Conus Albers +Equal Area) — the projection NRCS and NASS use for lower-48 area +calculations, so the number matches what soil-survey and yield tooling +would report for the same parcel.

+

acres remains editable as a fallback: fields without a digitized +boundary keep working with a manual estimate, and the compute only fires +once a polygon is set.

+

Scope. This module ships a single GeoPolygon per field — +non-contiguous plots (two disconnected polygons that are +administratively one “field”) should be recorded as separate +farm.field records for now. A future farm_field_multi_polygon +extension can promote the column to GeoMultiPolygon if the demand +materializes.

+

Install requirement. The Postgres instance must have PostGIS +enabled. base_geoengine declares the extension; install will fail +cleanly if the extension is missing. The base farm_field module +stays usable without PostGIS — install this extension only when geometry +support is wanted.

+

Table of contents

+ +
+

Usage

+
    +
  1. Open Farm → Fields → Fields and pick a field.
  2. +
  3. The Boundary notebook tab renders a Leaflet map of the field’s +polygon.
  4. +
  5. Set the polygon via the geoengine map view (vertex drag works there) +or by importing a WKT string. In-form vertex editing through the +boundary tab arrives once web_leaflet_draw_lib is migrated to +Odoo 19.
  6. +
  7. Save. The Acres field auto-recomputes from the polygon area, +reprojected to EPSG:5070 Albers Equal Area.
  8. +
  9. Switch the action’s view to Map (geoengine) to see every field at +once.
  10. +
+

The acres field stays editable as a fallback: fields without a +digitized boundary can carry a manual estimate. Once a polygon is set, +the compute overrides the manual value.

+

Multi-plot fields (a “north 40” that is two non-contiguous polygons) +need GeoMultiPolygon rather than GeoPolygon — out of scope for +the MVP; record each plot as its own farm.field for now.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ledo Enterprises
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

dnplkndll

+

This module is part of the ledoent/farm-pack project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/farm_field_geo/tests/__init__.py b/farm_field_geo/tests/__init__.py new file mode 100644 index 0000000..93da03c --- /dev/null +++ b/farm_field_geo/tests/__init__.py @@ -0,0 +1 @@ +from . import test_farm_field_geo diff --git a/farm_field_geo/tests/test_farm_field_geo.py b/farm_field_geo/tests/test_farm_field_geo.py new file mode 100644 index 0000000..93a0a45 --- /dev/null +++ b/farm_field_geo/tests/test_farm_field_geo.py @@ -0,0 +1,94 @@ +from odoo.tests.common import TransactionCase + + +def _polygon_wkt(min_lon, min_lat, max_lon, max_lat): + """Build a WGS84 rectangular polygon as WKT. + + Saves repeating the SRID prefix and the close-the-ring vertex across tests. + """ + return ( + "SRID=4326;POLYGON((" + f"{min_lon} {min_lat}, " + f"{max_lon} {min_lat}, " + f"{max_lon} {max_lat}, " + f"{min_lon} {max_lat}, " + f"{min_lon} {min_lat}" + "))" + ) + + +# Reference polygon: ~0.01° square around Ligonier PA (40.242N, 79.245W). +# Albers reprojection of this rectangle is ~233 acres — the assertion ranges +# in the tests allow ±15% slack for projection edge cases. +LIGONIER_BIG = _polygon_wkt(-79.245, 40.242, -79.235, 40.252) +LIGONIER_SMALL = _polygon_wkt(-79.245, 40.242, -79.244, 40.243) + + +class TestFarmFieldGeo(TransactionCase): + """Verify the polygon → acreage compute and its interaction with manual entry.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.farm = cls.env["res.partner"].create( + {"name": "Test Farm", "is_company": True} + ) + cls.crop = cls.env["farm.crop"].create({"name": "Corn", "code": "CORN"}) + + def _make_field(self, geom=None, acres=None): + vals = { + "name": "North 40", + "farm_partner_id": self.farm.id, + "crop_id": self.crop.id, + } + if geom is not None: + vals["geom"] = geom + if acres is not None: + vals["acres"] = acres + return self.env["farm.field"].create(vals) + + def test_acres_manual_entry_persists_without_geom(self): + # No polygon drawn yet — the user can set acres manually and the + # compute must leave the manual value untouched. (The parent + # `farm.field.acres` already defaults to 0.0, so to actually probe + # the compute behaviour we set a distinctive value here.) + field = self._make_field(acres=12.5) + field.invalidate_recordset() + self.assertEqual(field.acres, 12.5) + + def test_acres_compute_from_polygon(self): + # ~0.01° square at 40N: one deg longitude is ~85 km here and one + # deg latitude is ~111 km, so the rectangle is ~850 m × 1110 m ≈ + # 943,500 m² ≈ 233 acres. Allow generous slack for projection. + field = self._make_field(geom=LIGONIER_BIG) + self.assertGreater(field.acres, 200.0) + self.assertLess(field.acres, 260.0) + + def test_acres_compute_overrides_manual_entry_on_geom_set(self): + # User started with a manual estimate, then drew the boundary. + # The compute should win. + field = self._make_field(acres=999.0) + self.assertEqual(field.acres, 999.0) + field.geom = LIGONIER_BIG + field.flush_recordset() + field.invalidate_recordset() + self.assertNotEqual(field.acres, 999.0) + self.assertGreater(field.acres, 200.0) + + def test_acres_recomputes_on_geom_change(self): + field = self._make_field(geom=LIGONIER_SMALL) + small_acres = field.acres + self.assertGreater(small_acres, 0.0) + + field.geom = LIGONIER_BIG + field.flush_recordset() + field.invalidate_recordset() + self.assertGreater( + field.acres, small_acres * 50, "Bigger polygon → bigger acreage" + ) + + def test_acres_deterministic_for_identical_polygon(self): + # Two fields with the same boundary must report the same acreage. + a = self._make_field(geom=LIGONIER_BIG) + b = self._make_field(geom=LIGONIER_BIG) + self.assertEqual(a.acres, b.acres) diff --git a/farm_field_geo/views/farm_field_views.xml b/farm_field_geo/views/farm_field_views.xml new file mode 100644 index 0000000..46fc4c3 --- /dev/null +++ b/farm_field_geo/views/farm_field_views.xml @@ -0,0 +1,46 @@ + + + + + farm.field.form.geo + farm.field + + + + + + + + + + + + + farm.field.geoengine + farm.field + + + + + + + + + + + + + + geoengine + + + + diff --git a/farm_quickbooks_io/views/res_config_settings_views.xml b/farm_quickbooks_io/views/res_config_settings_views.xml index 7073706..5b66453 100644 --- a/farm_quickbooks_io/views/res_config_settings_views.xml +++ b/farm_quickbooks_io/views/res_config_settings_views.xml @@ -36,7 +36,11 @@ QuickBooks Settings res.config.settings form - inline + {'module': 'farm_quickbooks_io'} diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..db16359 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,11 @@ +# [DON'T MERGE STRAIGHT TO MAIN] Pin OCA/geospatial 19.0 MIG branches that +# the farm_field_geo / farm_field_overlays modules depend on. Both modules +# fail the OCA `Detect unreleased dependencies` check while these upstream +# PRs are still open — accepted trade-off per the documented convention. +# +# Remove the pins once the upstream MIGs land: +# - base_geoengine: OCA/geospatial PR #446 (leNeo) +# - web_leaflet_lib + web_leaflet_draw_lib: ledoent/geospatial PR #1 +odoo-addon-base_geoengine @ git+https://github.com/leNeo/geospatial.git@19.0#subdirectory=base_geoengine +odoo-addon-web_leaflet_lib @ git+https://github.com/ledoent/geospatial.git@19.0-mig-web_leaflet_lib#subdirectory=web_leaflet_lib +odoo-addon-web_leaflet_draw_lib @ git+https://github.com/ledoent/geospatial.git@19.0-mig-web_leaflet_lib#subdirectory=web_leaflet_draw_lib