From 97817607f249d4236fe39b187a5beb02ac2e183c Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Sun, 17 May 2026 13:08:18 -0400
Subject: [PATCH 1/4] feat(farm_field_geo): PostGIS polygon + auto-computed
acreage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Extends farm.field with a `geom` GeoPolygon column (WGS84/4326) and
overrides `acres` to be auto-computed from polygon area, reprojected to
EPSG:5070 (Conus Albers Equal Area) — the projection NRCS and NASS use
for lower-48 acreage. Without this module the base field's `acres` is a
manual decimal; with it, drawing a boundary updates the number
automatically and editable as a fallback (readonly=False + store=True).
Depends on OCA/geospatial's `base_geoengine`. That module isn't on the
upstream 19.0 branch yet — workspace `repos.yaml` forward-port-pins to
OCA PR #446. Reverts to plain `oca 19.0` once that lands.
In-form polygon EDITING needs `web_leaflet_draw_lib`, which has no 19.0
MIG PR yet — for now the map widget renders read-only via base_geoengine
alone. Drawing/editing wires up once that horizontal migrates upstream
(or we migrate it ourselves in a follow-up).
---
farm_field_geo/README.rst | 92 ++++
farm_field_geo/__init__.py | 1 +
farm_field_geo/__manifest__.py | 19 +
farm_field_geo/models/__init__.py | 1 +
farm_field_geo/models/farm_field.py | 37 ++
farm_field_geo/pyproject.toml | 3 +
farm_field_geo/readme/CONTRIBUTORS.md | 1 +
farm_field_geo/readme/DESCRIPTION.md | 10 +
farm_field_geo/readme/USAGE.md | 6 +
farm_field_geo/readme/newsfragments/.gitkeep | 0
farm_field_geo/static/description/index.html | 444 +++++++++++++++++++
farm_field_geo/tests/__init__.py | 1 +
farm_field_geo/tests/test_farm_field_geo.py | 92 ++++
farm_field_geo/views/farm_field_views.xml | 36 ++
14 files changed, 743 insertions(+)
create mode 100644 farm_field_geo/README.rst
create mode 100644 farm_field_geo/__init__.py
create mode 100644 farm_field_geo/__manifest__.py
create mode 100644 farm_field_geo/models/__init__.py
create mode 100644 farm_field_geo/models/farm_field.py
create mode 100644 farm_field_geo/pyproject.toml
create mode 100644 farm_field_geo/readme/CONTRIBUTORS.md
create mode 100644 farm_field_geo/readme/DESCRIPTION.md
create mode 100644 farm_field_geo/readme/USAGE.md
create mode 100644 farm_field_geo/readme/newsfragments/.gitkeep
create mode 100644 farm_field_geo/static/description/index.html
create mode 100644 farm_field_geo/tests/__init__.py
create mode 100644 farm_field_geo/tests/test_farm_field_geo.py
create mode 100644 farm_field_geo/views/farm_field_views.xml
diff --git a/farm_field_geo/README.rst b/farm_field_geo/README.rst
new file mode 100644
index 0000000..e61a06f
--- /dev/null
+++ b/farm_field_geo/README.rst
@@ -0,0 +1,92 @@
+.. 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, draw a polygon on a Leaflet map and the acreage updates from
+the polygon area (reprojected to EPSG:5070 Albers Equal Area, the
+projection NRCS and NASS use for the lower 48).
+
+The base ``farm_field`` module remains usable without PostGIS — install
+this extension only when your deployment has a PostGIS-enabled Postgres.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+1. Open Farm → Fields → Fields and pick a field.
+2. On the form, the **Boundary** field renders a Leaflet map.
+3. Draw the field perimeter as a polygon (read-only viewer for now;
+ in-form drawing arrives once ``web_leaflet_draw_lib`` is migrated to
+ Odoo 19).
+4. Save. The **Acres** field auto-recomputes from the polygon area.
+5. Switch the action's view to **Map** to see every field on one canvas.
+
+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..7cf8836
--- /dev/null
+++ b/farm_field_geo/readme/DESCRIPTION.md
@@ -0,0 +1,10 @@
+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,
+draw a polygon on a Leaflet map and the acreage updates from the polygon area
+(reprojected to EPSG:5070 Albers Equal Area, the projection NRCS and NASS use
+for the lower 48).
+
+The base `farm_field` module remains usable without PostGIS — install this
+extension only when your deployment has a PostGIS-enabled Postgres.
diff --git a/farm_field_geo/readme/USAGE.md b/farm_field_geo/readme/USAGE.md
new file mode 100644
index 0000000..2309ea7
--- /dev/null
+++ b/farm_field_geo/readme/USAGE.md
@@ -0,0 +1,6 @@
+1. Open Farm → Fields → Fields and pick a field.
+2. On the form, the **Boundary** field renders a Leaflet map.
+3. Draw the field perimeter as a polygon (read-only viewer for now; in-form
+ drawing arrives once `web_leaflet_draw_lib` is migrated to Odoo 19).
+4. Save. The **Acres** field auto-recomputes from the polygon area.
+5. Switch the action's view to **Map** to see every field on one canvas.
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..78ff7af
--- /dev/null
+++ b/farm_field_geo/static/description/index.html
@@ -0,0 +1,444 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+
Farm Field — Geospatial
+
+

+
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, draw a polygon on a Leaflet map and the acreage updates from
+the polygon area (reprojected to EPSG:5070 Albers Equal Area, the
+projection NRCS and NASS use for the lower 48).
+
The base farm_field module remains usable without PostGIS — install
+this extension only when your deployment has a PostGIS-enabled Postgres.
+
Table of contents
+
+
+
+
+- Open Farm → Fields → Fields and pick a field.
+- On the form, the Boundary field renders a Leaflet map.
+- Draw the field perimeter as a polygon (read-only viewer for now;
+in-form drawing arrives once web_leaflet_draw_lib is migrated to
+Odoo 19).
+- Save. The Acres field auto-recomputes from the polygon area.
+- Switch the action’s view to Map to see every field on one canvas.
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
Current maintainer:
+

+
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..ce47bf9
--- /dev/null
+++ b/farm_field_geo/tests/test_farm_field_geo.py
@@ -0,0 +1,92 @@
+from odoo.tests.common import TransactionCase
+
+
+class TestFarmFieldGeo(TransactionCase):
+ """Verify the polygon → acreage compute.
+
+ Reference polygon: ~40 acres around Ligonier PA (40.2421N, 79.2389W).
+ The acreage we expect was hand-checked against USDA NRCS Web Soil Survey
+ for the same shape — small differences from a perfect 40 acres are fine,
+ we just want to confirm we're within a few percent and not 10x off.
+ """
+
+ @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):
+ return self.env["farm.field"].create(
+ {
+ "name": "North 40",
+ "farm_partner_id": self.farm.id,
+ "crop_id": self.crop.id,
+ "geom": geom,
+ }
+ )
+
+ def test_acres_zero_when_no_geom(self):
+ field = self._make_field()
+ self.assertEqual(field.acres, 0.0, "Empty polygon should report 0 acres")
+
+ def test_acres_manual_entry_preserved_without_geom(self):
+ # Without a polygon, the user can still set acres manually — the
+ # compute must not zero it out.
+ field = self._make_field()
+ field.acres = 12.5
+ field.flush_recordset()
+ field.invalidate_recordset()
+ self.assertEqual(field.acres, 12.5)
+
+ def test_acres_compute_from_polygon(self):
+ # Roughly 0.001 deg square around 40N, 79W. At that latitude one
+ # degree of longitude is ~85 km and one degree of latitude is ~111 km,
+ # so a 0.01 x 0.01 deg square is ~850 m x 1110 m = ~943,500 m^2 =
+ # ~233 acres. Use Albers reprojection for the real number.
+ geom = (
+ "SRID=4326;POLYGON(("
+ "-79.245 40.242, "
+ "-79.235 40.242, "
+ "-79.235 40.252, "
+ "-79.245 40.252, "
+ "-79.245 40.242"
+ "))"
+ )
+ field = self._make_field(geom=geom)
+ # ~230-240 acres range — exact value depends on Albers reprojection,
+ # but it should never be near zero or in the thousands.
+ self.assertGreater(field.acres, 200.0)
+ self.assertLess(field.acres, 260.0)
+
+ def test_acres_recomputes_on_geom_change(self):
+ small = (
+ "SRID=4326;POLYGON(("
+ "-79.245 40.242, "
+ "-79.244 40.242, "
+ "-79.244 40.243, "
+ "-79.245 40.243, "
+ "-79.245 40.242"
+ "))"
+ )
+ field = self._make_field(geom=small)
+ small_acres = field.acres
+ self.assertGreater(small_acres, 0.0)
+
+ big = (
+ "SRID=4326;POLYGON(("
+ "-79.245 40.242, "
+ "-79.235 40.242, "
+ "-79.235 40.252, "
+ "-79.245 40.252, "
+ "-79.245 40.242"
+ "))"
+ )
+ field.geom = big
+ field.flush_recordset()
+ field.invalidate_recordset()
+ self.assertGreater(
+ field.acres, small_acres * 50, "Bigger polygon → bigger acreage"
+ )
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..de11e2b
--- /dev/null
+++ b/farm_field_geo/views/farm_field_views.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ farm.field.form.geo
+ farm.field
+
+
+
+
+
+
+
+
+
+
+ farm.field.geoengine
+ farm.field
+
+
+
+
+
+
+
+
+
+
+
+
+
+ geoengine
+
+
+
+
From 137110615472e6c5829e9f1f6822e45a770254fc Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Sun, 17 May 2026 13:31:45 -0400
Subject: [PATCH 2/4] refactor(farm_field_geo): self-review fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Self-review against feature-completeness + DRY + test-coverage + docs:
- View: move the geom widget from after-acres (inside a 2-column group, where
the map gets crushed) to its own Boundary notebook tab where it can use
full width. Add `geom_field="geom"` to the geoengine view as the canonical
way to declare the plotted column even when the model only has one.
- Tests: extract a `_polygon_wkt(min_lon, min_lat, max_lon, max_lat)` helper
so the SRID prefix + close-the-ring vertex stop repeating. Drop the weak
zero-acres test (was indistinguishable from the parent Float's default).
Add `test_acres_compute_overrides_manual_entry_on_geom_set` (manual
estimate → polygon set → compute wins) and
`test_acres_deterministic_for_identical_polygon` (two fields, same
boundary, same acres).
- USAGE.md: untangle the contradictory "draw … read-only" wording. Make the
manual-acres-as-fallback flow explicit.
- DESCRIPTION.md: add Scope (multi-plot fields → multiple farm.field rows
for MVP) and Install Requirement (PostGIS must be enabled) sections so a
reviewer doesn't have to read the code to learn the boundary.
---
farm_field_geo/README.rst | 52 ++++++--
farm_field_geo/readme/DESCRIPTION.md | 23 +++-
farm_field_geo/readme/USAGE.md | 20 ++-
farm_field_geo/static/description/index.html | 46 +++++--
farm_field_geo/tests/test_farm_field_geo.py | 124 ++++++++++---------
farm_field_geo/views/farm_field_views.xml | 20 ++-
6 files changed, 186 insertions(+), 99 deletions(-)
diff --git a/farm_field_geo/README.rst b/farm_field_geo/README.rst
index e61a06f..34c66cf 100644
--- a/farm_field_geo/README.rst
+++ b/farm_field_geo/README.rst
@@ -30,12 +30,28 @@ 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, draw a polygon on a Leaflet map and the acreage updates from
-the polygon area (reprojected to EPSG:5070 Albers Equal Area, the
-projection NRCS and NASS use for the lower 48).
-
-The base ``farm_field`` module remains usable without PostGIS — install
-this extension only when your deployment has a PostGIS-enabled Postgres.
+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**
@@ -46,12 +62,24 @@ Usage
=====
1. Open Farm → Fields → Fields and pick a field.
-2. On the form, the **Boundary** field renders a Leaflet map.
-3. Draw the field perimeter as a polygon (read-only viewer for now;
- in-form drawing arrives once ``web_leaflet_draw_lib`` is migrated to
- Odoo 19).
-4. Save. The **Acres** field auto-recomputes from the polygon area.
-5. Switch the action's view to **Map** to see every field on one canvas.
+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
===========
diff --git a/farm_field_geo/readme/DESCRIPTION.md b/farm_field_geo/readme/DESCRIPTION.md
index 7cf8836..2f68ede 100644
--- a/farm_field_geo/readme/DESCRIPTION.md
+++ b/farm_field_geo/readme/DESCRIPTION.md
@@ -2,9 +2,22 @@ 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,
-draw a polygon on a Leaflet map and the acreage updates from the polygon area
-(reprojected to EPSG:5070 Albers Equal Area, the projection NRCS and NASS use
-for the lower 48).
+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.
-The base `farm_field` module remains usable without PostGIS — install this
-extension only when your deployment has a PostGIS-enabled Postgres.
+`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
index 2309ea7..61bd247 100644
--- a/farm_field_geo/readme/USAGE.md
+++ b/farm_field_geo/readme/USAGE.md
@@ -1,6 +1,16 @@
1. Open Farm → Fields → Fields and pick a field.
-2. On the form, the **Boundary** field renders a Leaflet map.
-3. Draw the field perimeter as a polygon (read-only viewer for now; in-form
- drawing arrives once `web_leaflet_draw_lib` is migrated to Odoo 19).
-4. Save. The **Acres** field auto-recomputes from the polygon area.
-5. Switch the action's view to **Map** to see every field on one canvas.
+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/static/description/index.html b/farm_field_geo/static/description/index.html
index 78ff7af..f604459 100644
--- a/farm_field_geo/static/description/index.html
+++ b/farm_field_geo/static/description/index.html
@@ -378,11 +378,25 @@ Farm Field — Geospatial
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, draw a polygon on a Leaflet map and the acreage updates from
-the polygon area (reprojected to EPSG:5070 Albers Equal Area, the
-projection NRCS and NASS use for the lower 48).
-The base farm_field module remains usable without PostGIS — install
-this extension only when your deployment has a PostGIS-enabled Postgres.
+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
@@ -400,13 +414,23 @@ Farm Field — Geospatial
- Open Farm → Fields → Fields and pick a field.
-- On the form, the Boundary field renders a Leaflet map.
-- Draw the field perimeter as a polygon (read-only viewer for now;
-in-form drawing arrives once web_leaflet_draw_lib is migrated to
-Odoo 19).
-- Save. The Acres field auto-recomputes from the polygon area.
-- Switch the action’s view to Map to see every field on one canvas.
+- The Boundary notebook tab renders a Leaflet map of the field’s
+polygon.
+- 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.
+- Save. The Acres field auto-recomputes from the polygon area,
+reprojected to EPSG:5070 Albers Equal Area.
+- 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/tests/test_farm_field_geo.py b/farm_field_geo/tests/test_farm_field_geo.py
index ce47bf9..93a0a45 100644
--- a/farm_field_geo/tests/test_farm_field_geo.py
+++ b/farm_field_geo/tests/test_farm_field_geo.py
@@ -1,14 +1,31 @@
from odoo.tests.common import TransactionCase
-class TestFarmFieldGeo(TransactionCase):
- """Verify the polygon → acreage compute.
+def _polygon_wkt(min_lon, min_lat, max_lon, max_lat):
+ """Build a WGS84 rectangular polygon as WKT.
- Reference polygon: ~40 acres around Ligonier PA (40.2421N, 79.2389W).
- The acreage we expect was hand-checked against USDA NRCS Web Soil Survey
- for the same shape — small differences from a perfect 40 acres are fine,
- we just want to confirm we're within a few percent and not 10x off.
+ 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):
@@ -18,75 +35,60 @@ def setUpClass(cls):
)
cls.crop = cls.env["farm.crop"].create({"name": "Corn", "code": "CORN"})
- def _make_field(self, geom=None):
- return self.env["farm.field"].create(
- {
- "name": "North 40",
- "farm_partner_id": self.farm.id,
- "crop_id": self.crop.id,
- "geom": geom,
- }
- )
-
- def test_acres_zero_when_no_geom(self):
- field = self._make_field()
- self.assertEqual(field.acres, 0.0, "Empty polygon should report 0 acres")
+ 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_preserved_without_geom(self):
- # Without a polygon, the user can still set acres manually — the
- # compute must not zero it out.
- field = self._make_field()
- field.acres = 12.5
- field.flush_recordset()
+ 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):
- # Roughly 0.001 deg square around 40N, 79W. At that latitude one
- # degree of longitude is ~85 km and one degree of latitude is ~111 km,
- # so a 0.01 x 0.01 deg square is ~850 m x 1110 m = ~943,500 m^2 =
- # ~233 acres. Use Albers reprojection for the real number.
- geom = (
- "SRID=4326;POLYGON(("
- "-79.245 40.242, "
- "-79.235 40.242, "
- "-79.235 40.252, "
- "-79.245 40.252, "
- "-79.245 40.242"
- "))"
- )
- field = self._make_field(geom=geom)
- # ~230-240 acres range — exact value depends on Albers reprojection,
- # but it should never be near zero or in the thousands.
+ # ~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):
- small = (
- "SRID=4326;POLYGON(("
- "-79.245 40.242, "
- "-79.244 40.242, "
- "-79.244 40.243, "
- "-79.245 40.243, "
- "-79.245 40.242"
- "))"
- )
- field = self._make_field(geom=small)
+ field = self._make_field(geom=LIGONIER_SMALL)
small_acres = field.acres
self.assertGreater(small_acres, 0.0)
- big = (
- "SRID=4326;POLYGON(("
- "-79.245 40.242, "
- "-79.235 40.242, "
- "-79.235 40.252, "
- "-79.245 40.252, "
- "-79.245 40.242"
- "))"
- )
- field.geom = big
+ 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
index de11e2b..46fc4c3 100644
--- a/farm_field_geo/views/farm_field_views.xml
+++ b/farm_field_geo/views/farm_field_views.xml
@@ -1,23 +1,33 @@
-
+
farm.field.form.geo
farm.field
-
-
+
+
+
+
-
+
farm.field.geoengine
farm.field
-
+
From 89d8d148f3fe5161b18d44051f4e1cded28d9d80 Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Mon, 18 May 2026 09:15:11 -0400
Subject: [PATCH 3/4] ci(farm_field_geo): pin OCA/geospatial unreleased deps
via test-requirements.txt
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
farm_field_geo depends on base_geoengine, which isn't on the upstream
OCA/geospatial 19.0 branch yet (PR #446 is open + green but unmerged).
Same story for web_leaflet_lib + web_leaflet_draw_lib, which we migrated
on the ledoent fork (PR #1 on ledoent/geospatial).
Adds a top-level test-requirements.txt with git+https pins for each.
OCA's `Detect unreleased dependencies` lint will go red against these
pins by design — accepted trade-off per the documented convention until
the upstream MIGs land.
When the chain lands, this file gets either deleted (best) or trimmed
to the pins that are still unreleased.
---
test-requirements.txt | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 test-requirements.txt
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
From 2d0556735894bd03d08545b08bdedbe5d9ba7ae6 Mon Sep 17 00:00:00 2001
From: Don Kendall
Date: Mon, 18 May 2026 09:18:20 -0400
Subject: [PATCH 4/4] fix(farm_quickbooks_io): drop deprecated target=inline on
settings action
Odoo 19 removed 'inline' from the allowed values of ir.actions.act_window.target,
causing ParseError on module install:
ValueError: Wrong value for ir.actions.act_window.target: 'inline'
Default target (current) is what every other res.config.settings action uses,
so just drop the line.
---
farm_quickbooks_io/views/res_config_settings_views.xml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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'}