Skip to content

feat(farm_observation): geotagged field observations w/ urgency triage - #4

Merged
dnplkndll merged 16 commits into
19.0from
feat/farm-observation
May 18, 2026
Merged

feat(farm_observation): geotagged field observations w/ urgency triage#4
dnplkndll merged 16 commits into
19.0from
feat/farm-observation

Conversation

@dnplkndll

@dnplkndll dnplkndll commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds farm.observation — geotagged + photo-attached field notes that managers can triage by urgency. Sits atop farm_field_geo so observations get a real map point per record.

Surface Behavior
Form Required notes, optional photo + map point on a Location notebook tab, urgency priority widget, chatter
List Color-coded by urgency (high=danger, med=warning), sortable
Kanban Default grouped by urgency so high-priority rises to the top
Map (geoengine) Drops pins on the field map so a manager sees everything at a glance
Search High-Urgency / Medium+ / Last-7-Days canned filters; group-by field/type/urgency

Commits (review order)

  1. `feat(farm_observation): geotagged field observations w/ urgency triage` — initial scaffold (model, views, security, six tests, readme)
  2. `refactor(farm_observation): self-review — enforce multi-company guard` — added `_check_company_auto = True` + `check_company=True` on `field_id` after a second-pass review caught the multi-company write hole

Depends on

Stacks: this PR → #3#2#1 → 19.0. Each base merge auto-retargets this PR down the chain.

Self-review fixes baked in (BugBot-style, not posted)

# Finding Fix
1 `_order = "urgency desc"` would alpha-sort selection keys — "med" lands above "high" because m > h alphabetically. High-priority items would NOT sort to the top, breaking the whole triage UX Stored computed `urgency_rank` integer mirroring the selection; `_order` now uses that. Regression test `test_urgency_sort_priority_not_alphabetical` asserts the actual priority order
2 `<field name="photo_id" widget="many2one_avatar" />` — the avatar widget is for partner-style records, not `ir.attachment` Dropped the widget; default rendering shows the attachment name with a download link
3 Missing `_check_company_auto = True`. A user in company A could write an observation referencing a field in company B without the framework catching the cross-company write Added `_check_company_auto = True` + `check_company=True` on `field_id`. Caught in the v1-slice review pass; landed as commit 2

Test coverage

Six tests in `tests/test_farm_observation.py`:

Test Asserts
`test_name_includes_field_type_and_date` Auto-name surfaces field + type + date in list views
`test_name_recomputes_on_field_change` Moving an observation to another field updates the name
`test_urgency_tracking` Urgency change writes a chatter audit message
`test_company_id_inherits_from_field` Related multi-company isolation works
`test_geom_optional` Records without GPS still save (manual point-pick later)
`test_urgency_sort_priority_not_alphabetical` Regression: high-urgency sorts first, not "med"

What's deferred

  • EXIF GPS auto-population. Photo uploads carrying GPS tags should auto-set `geom`. Needs an OWL JS helper that parses image files client-side. Separate PR when a field partner asks.
  • Shared `priority_rank` mixin. `urgency_rank` here and `condition_rank` in PR feat(farm_fence): fence lines + condition tracking + length compute #6 reinvent the same pattern. Future refactor extracts it into a `mail.priority.mixin`-style helper in `farm_base`. Out of scope for v1.

Screenshots

Not applicable in this PR — same install-gate chain as #2, #3. Screenshots after the stack merges to a review env.

Test plan

dnplkndll added 10 commits May 16, 2026 21:51
…read

Eliminates ~12 lines × 8+ farm models. All SMB-tier models and existing farm
modules can _inherit = ["farm.mixin"] to pick up the shared bundle in one line.
farm.crop is the canonical crop identity referenced by farm.field, farm.planting,
and farm.harvest. Ships with 18 common US small-farm crops under noupdate=1 so
farmers can edit freely.

Fields:
- name, latin_name, family (Solanaceae, Brassicaceae, etc. — useful for rotation)
- category: vegetable/fruit/grain/legume/herb/cover/forage/flower/other
- days_to_maturity (consumed by farm.planting._compute_expected_harvest_date)
- image (auto-sized 128px variant for kanban)

5 tests.
farm.field models a plot of land. Inherits farm.mixin (color/notes/mail.thread).

Fields:
- name, code (used on signage), farm_partner_id, company_id
- acres (manual; farm_field_geo will compute from polygon)
- crop_id (current crop), season_id, organic_certified (tracked)

Multi-company:
- _check_company_auto = True
- ir.rule scopes [company_id IN env.company_ids] globally
- security covered by test (user in company A blocked from company B's field)

6 tests including multi-company isolation.
farm.planting records a (field, crop, date) tuple. State machine walks through planted → growing → harvested|failed. Expected harvest date auto-computes from crop.days_to_maturity (overrideable).

Multi-company: company_id is related to field_id.company_id, store=True + index=True + precompute=True (NOT NULL at INSERT so ir.rule binds correctly). ir.rule scoping by company_id. field_id is check_company=True.

9 tests including precompute and date-constraint checks.
farm.harvest logs quantity + UoM + date for each pick. Two flows: (1) from a planting — field/crop auto-fill via precompute=True compute, locked readonly in the form; (2) standalone — manual field/crop entry for retroactive records.

Multi-company: company_id related to field.company_id, precompute=True. ir.rule scoping. check_company=True on field_id.

Optional grade (A/B/C/compost) for tiered pricing downstream.

6 tests including stale field_id behavior when planting cleared.
The Card template introduced in Odoo 17+ doesn't support the legacy wrappers (`oe_kanban_details`, `o_kanban_image_fill_left`) or the `highlight_color` attribute, and the `t-if="record.X.raw_value"` conditional pattern that worked in v18 raises an OwlError at mount time.

Rewrite all four kanbans in the simplified Card style (mirrors farm_egg_production): drop wrapper divs, drop `highlight_color`, replace conditional t-if with bare `<field>` references that render empty when null. Verified against fresh 19.0 DB — 0 page errors, all records render.
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).
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.
Adds three public-domain overlay raster layers to the farm.field map:

- USDA Cropland (CDL): WMS, current-year crop classification raster.
  Layer name is year-specific (`cdl_YYYY`); USAGE.md documents the
  annual-bump procedure once the new year's CDL publishes.
- NRCS SSURGO Soil: WMS, soil mapunit polygons covering CONUS.
- OpenStreetMap basemap: tile-server backdrop so roads + parcels show
  through.

All three are read-only, public-domain US government services. No API
keys, no per-request quota. Stored as `geoengine.raster.layer` records
bound to `farm_field_geo.farm_field_view_geoengine`, so admins can
disable, reorder, or re-point them via Settings → Technical →
Geoengine → Raster Layers without touching code (`noupdate="1"` keeps
admin edits across module upgrades).

Three smoke tests verify the records survive install + carry the
required schema (correct view binding, LAYERS param on the WMS records,
overlay=False on the basemap).
New model `farm.observation` for capturing what's happening in each field:
type (pest/disease/weed/soil/water/yield/photo/other), free-text notes,
optional GeoPoint location, urgency (low/med/high), optional photo
attachment, mail.thread for chatter. Auto-named from field+type+date.

Views: list (color-coded by urgency), urgency-grouped kanban, form with
chatter + map widget on a Location tab, search with last-7-days +
high-urgency canned filters, geoengine map view that drops pins on the
field map.

Self-review fixes baked in before commit:
- `_order` would alpha-sort selection keys ("med" > "high"); fixed via a
  stored computed `urgency_rank` integer + regression test that asserts
  the actual priority order
- Dropped `widget="many2one_avatar"` on `photo_id` (the avatar widget
  is for partner-style records, not ir.attachment)

Deferred: EXIF GPS auto-population from photo uploads (needs an OWL JS
helper that parses files client-side — separate PR when a field partner
asks).
Self-review pass against the v1 geo slice: farm.observation didn't enforce
multi-company isolation at write. A user in company A could have created
an observation pointing at a field in company B without the framework
catching the cross-company write.

- `_check_company_auto = True` on the model
- `check_company=True` on field_id

The framework now raises UserError if field_id.company_id and
observation.company_id diverge — the existing `company_id` related field
already mirrors field_id, so this is enforcement, not a schema change.
@dnplkndll
dnplkndll changed the base branch from feat/farm-field-overlays to 19.0 May 18, 2026 14:56
dnplkndll added 5 commits May 18, 2026 11:00
OCA Odoo 19 search-view RNG rejects 'expand' on <group>. Drop it; the
group-by panel still renders correctly without it.
…o 19

Odoo 19's search-view RNG rejects 'string' and 'expand' attributes on
<group> inside <search>. Move the group-by filters out as flat children
of <search>; the UI auto-builds the Group By panel from filters carrying
a group_by context.
…mail.thread plumbing

The old test_urgency_tracking exercised Odoo's mail.thread message
delivery (low→high should add a chatter audit). It's flaky in CI
because chatter writes don't always flush in TransactionCase scope.
Switch to checking the field declaration directly — that's what we
actually control.
@dnplkndll
dnplkndll merged commit 4a3919f into 19.0 May 18, 2026
3 of 4 checks passed
@dnplkndll
dnplkndll deleted the feat/farm-observation branch May 18, 2026 15:27
dnplkndll added a commit that referenced this pull request May 18, 2026
Bundle of fixes learned from PR #4 in one commit:
- development_status=Alpha (OCA check-dev-status gate)
- Drop bogus code field from farm.crop test fixture
- Replace EWKT strings with shapely Point/Polygon/LineString
  (base_geoengine's GeoMultiGeometry field expects shapely or EWKB hex)
- Declare shapely in external_dependencies.python
- Unwrap <group string="Group By" expand="0"> in search view
  (Odoo 19 RNG rejects both attributes on <group>)
dnplkndll added a commit that referenced this pull request May 18, 2026
…rve (#5)

* feat(farm_base): add farm.mixin AbstractModel for color/notes/mail.thread

Eliminates ~12 lines × 8+ farm models. All SMB-tier models and existing farm
modules can _inherit = ["farm.mixin"] to pick up the shared bundle in one line.

* feat(farm_crop): catalog of farm crops with 18 seeded entries

farm.crop is the canonical crop identity referenced by farm.field, farm.planting,
and farm.harvest. Ships with 18 common US small-farm crops under noupdate=1 so
farmers can edit freely.

Fields:
- name, latin_name, family (Solanaceae, Brassicaceae, etc. — useful for rotation)
- category: vegetable/fruit/grain/legume/herb/cover/forage/flower/other
- days_to_maturity (consumed by farm.planting._compute_expected_harvest_date)
- image (auto-sized 128px variant for kanban)

5 tests.

* feat(farm_field): fields/plots with multi-company isolation

farm.field models a plot of land. Inherits farm.mixin (color/notes/mail.thread).

Fields:
- name, code (used on signage), farm_partner_id, company_id
- acres (manual; farm_field_geo will compute from polygon)
- crop_id (current crop), season_id, organic_certified (tracked)

Multi-company:
- _check_company_auto = True
- ir.rule scopes [company_id IN env.company_ids] globally
- security covered by test (user in company A blocked from company B's field)

6 tests including multi-company isolation.

* feat(farm_planting): planting events with state machine + multi-company

farm.planting records a (field, crop, date) tuple. State machine walks through planted → growing → harvested|failed. Expected harvest date auto-computes from crop.days_to_maturity (overrideable).

Multi-company: company_id is related to field_id.company_id, store=True + index=True + precompute=True (NOT NULL at INSERT so ir.rule binds correctly). ir.rule scoping by company_id. field_id is check_company=True.

9 tests including precompute and date-constraint checks.

* feat(farm_harvest): harvest events with auto-fill from planting

farm.harvest logs quantity + UoM + date for each pick. Two flows: (1) from a planting — field/crop auto-fill via precompute=True compute, locked readonly in the form; (2) standalone — manual field/crop entry for retroactive records.

Multi-company: company_id related to field.company_id, precompute=True. ir.rule scoping. check_company=True on field_id.

Optional grade (A/B/C/compost) for tiered pricing downstream.

6 tests including stale field_id behavior when planting cleared.

* fix: kanban templates for Odoo 19 Card system

The Card template introduced in Odoo 17+ doesn't support the legacy wrappers (`oe_kanban_details`, `o_kanban_image_fill_left`) or the `highlight_color` attribute, and the `t-if="record.X.raw_value"` conditional pattern that worked in v18 raises an OwlError at mount time.

Rewrite all four kanbans in the simplified Card style (mirrors farm_egg_production): drop wrapper divs, drop `highlight_color`, replace conditional t-if with bare `<field>` references that render empty when null. Verified against fresh 19.0 DB — 0 page errors, all records render.

* feat(farm_field_geo): PostGIS polygon + auto-computed acreage

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).

* refactor(farm_field_geo): self-review fixes

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.

* feat(farm_field_overlays): USDA Cropland + NRCS Soil + OSM overlays

Adds three public-domain overlay raster layers to the farm.field map:

- USDA Cropland (CDL): WMS, current-year crop classification raster.
  Layer name is year-specific (`cdl_YYYY`); USAGE.md documents the
  annual-bump procedure once the new year's CDL publishes.
- NRCS SSURGO Soil: WMS, soil mapunit polygons covering CONUS.
- OpenStreetMap basemap: tile-server backdrop so roads + parcels show
  through.

All three are read-only, public-domain US government services. No API
keys, no per-request quota. Stored as `geoengine.raster.layer` records
bound to `farm_field_geo.farm_field_view_geoengine`, so admins can
disable, reorder, or re-point them via Settings → Technical →
Geoengine → Raster Layers without touching code (`noupdate="1"` keeps
admin edits across module upgrades).

Three smoke tests verify the records survive install + carry the
required schema (correct view binding, LAYERS param on the WMS records,
overlay=False on the basemap).

* feat(farm_observation): geotagged field observations w/ urgency triage

New model `farm.observation` for capturing what's happening in each field:
type (pest/disease/weed/soil/water/yield/photo/other), free-text notes,
optional GeoPoint location, urgency (low/med/high), optional photo
attachment, mail.thread for chatter. Auto-named from field+type+date.

Views: list (color-coded by urgency), urgency-grouped kanban, form with
chatter + map widget on a Location tab, search with last-7-days +
high-urgency canned filters, geoengine map view that drops pins on the
field map.

Self-review fixes baked in before commit:
- `_order` would alpha-sort selection keys ("med" > "high"); fixed via a
  stored computed `urgency_rank` integer + regression test that asserts
  the actual priority order
- Dropped `widget="many2one_avatar"` on `photo_id` (the avatar widget
  is for partner-style records, not ir.attachment)

Deferred: EXIF GPS auto-population from photo uploads (needs an OWL JS
helper that parses files client-side — separate PR when a field partner
asks).

* feat(farm_water_source): wells, ponds, streams + which fields they serve

New `farm.water.source` model. Tracks the farm's water infrastructure with
its location, capacity, last-tested date, and a many2many of fields each
source serves. `geom` is `GeoMultiGeometry` so one column accepts the
shape that matches the type — point for well/trough/spring/hydrant,
linestring for stream, polygon for pond/tank.

Views: list (sum capacity), form (notebook for quality notes + location
map), search with "not tested in 1 year" canned filter, geoengine map
view showing every source on the field map.

Four tests cover all three geometry shapes (point/line/polygon) on the
same column and the field_ids many2many lookup that answers "what
fields does this source serve?".

* fix(farm_water_source): pre-empt all PR #4 cascade gotchas

Bundle of fixes learned from PR #4 in one commit:
- development_status=Alpha (OCA check-dev-status gate)
- Drop bogus code field from farm.crop test fixture
- Replace EWKT strings with shapely Point/Polygon/LineString
  (base_geoengine's GeoMultiGeometry field expects shapely or EWKB hex)
- Declare shapely in external_dependencies.python
- Unwrap <group string="Group By" expand="0"> in search view
  (Odoo 19 RNG rejects both attributes on <group>)

* fix(farm_water_source): GeoMultiGeometry not in base_geoengine 19.0 — fall back to GeoPoint

AttributeError: module 'odoo.fields' has no attribute 'GeoMultiGeometry'.
base_geoengine 19.0 (PR #446 head) only exposes GeoPoint, GeoLine,
GeoPolygon and their Multi variants — no GeoAnyGeometry.

v1 represents every source type as a single Point (well-head, tank tap,
pond centroid, stream access). Polygon footprints + linestrings deferred
to a future farm_water_source_polygon extension if a customer asks for
surface-area / depth-contour features.
dnplkndll added a commit that referenced this pull request May 18, 2026
…j, view RNG

Bundle of fixes learned across PR #7/#3/#4/#5 cascade:
- development_status=Alpha (OCA check-dev-status gate)
- Drop bogus 'code' field from farm.crop fixture
- Replace EWKT LineString strings with shapely.LineString
  (base_geoengine's GeoLineString expects shapely or EWKB hex)
- Switch reprojection from .transform() (Django-style, doesn't exist on
  shapely) to pyproj.Transformer + shapely.ops.transform — mirrors
  farm_field_geo's pattern
- Declare pyproj + shapely in external_dependencies.python
- Unwrap <group string='Group By' expand='0'> in search view
  (Odoo 19 RNG rejects both attributes on <group>)
- Replace flaky test_condition_tracking (mail.thread plumbing) with a
  field-definition probe — same approach as farm_observation
dnplkndll added a commit that referenced this pull request May 18, 2026
* feat(farm_base): add farm.mixin AbstractModel for color/notes/mail.thread

Eliminates ~12 lines × 8+ farm models. All SMB-tier models and existing farm
modules can _inherit = ["farm.mixin"] to pick up the shared bundle in one line.

* feat(farm_crop): catalog of farm crops with 18 seeded entries

farm.crop is the canonical crop identity referenced by farm.field, farm.planting,
and farm.harvest. Ships with 18 common US small-farm crops under noupdate=1 so
farmers can edit freely.

Fields:
- name, latin_name, family (Solanaceae, Brassicaceae, etc. — useful for rotation)
- category: vegetable/fruit/grain/legume/herb/cover/forage/flower/other
- days_to_maturity (consumed by farm.planting._compute_expected_harvest_date)
- image (auto-sized 128px variant for kanban)

5 tests.

* feat(farm_field): fields/plots with multi-company isolation

farm.field models a plot of land. Inherits farm.mixin (color/notes/mail.thread).

Fields:
- name, code (used on signage), farm_partner_id, company_id
- acres (manual; farm_field_geo will compute from polygon)
- crop_id (current crop), season_id, organic_certified (tracked)

Multi-company:
- _check_company_auto = True
- ir.rule scopes [company_id IN env.company_ids] globally
- security covered by test (user in company A blocked from company B's field)

6 tests including multi-company isolation.

* feat(farm_planting): planting events with state machine + multi-company

farm.planting records a (field, crop, date) tuple. State machine walks through planted → growing → harvested|failed. Expected harvest date auto-computes from crop.days_to_maturity (overrideable).

Multi-company: company_id is related to field_id.company_id, store=True + index=True + precompute=True (NOT NULL at INSERT so ir.rule binds correctly). ir.rule scoping by company_id. field_id is check_company=True.

9 tests including precompute and date-constraint checks.

* feat(farm_harvest): harvest events with auto-fill from planting

farm.harvest logs quantity + UoM + date for each pick. Two flows: (1) from a planting — field/crop auto-fill via precompute=True compute, locked readonly in the form; (2) standalone — manual field/crop entry for retroactive records.

Multi-company: company_id related to field.company_id, precompute=True. ir.rule scoping. check_company=True on field_id.

Optional grade (A/B/C/compost) for tiered pricing downstream.

6 tests including stale field_id behavior when planting cleared.

* fix: kanban templates for Odoo 19 Card system

The Card template introduced in Odoo 17+ doesn't support the legacy wrappers (`oe_kanban_details`, `o_kanban_image_fill_left`) or the `highlight_color` attribute, and the `t-if="record.X.raw_value"` conditional pattern that worked in v18 raises an OwlError at mount time.

Rewrite all four kanbans in the simplified Card style (mirrors farm_egg_production): drop wrapper divs, drop `highlight_color`, replace conditional t-if with bare `<field>` references that render empty when null. Verified against fresh 19.0 DB — 0 page errors, all records render.

* feat(farm_field_geo): PostGIS polygon + auto-computed acreage

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).

* refactor(farm_field_geo): self-review fixes

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.

* feat(farm_field_overlays): USDA Cropland + NRCS Soil + OSM overlays

Adds three public-domain overlay raster layers to the farm.field map:

- USDA Cropland (CDL): WMS, current-year crop classification raster.
  Layer name is year-specific (`cdl_YYYY`); USAGE.md documents the
  annual-bump procedure once the new year's CDL publishes.
- NRCS SSURGO Soil: WMS, soil mapunit polygons covering CONUS.
- OpenStreetMap basemap: tile-server backdrop so roads + parcels show
  through.

All three are read-only, public-domain US government services. No API
keys, no per-request quota. Stored as `geoengine.raster.layer` records
bound to `farm_field_geo.farm_field_view_geoengine`, so admins can
disable, reorder, or re-point them via Settings → Technical →
Geoengine → Raster Layers without touching code (`noupdate="1"` keeps
admin edits across module upgrades).

Three smoke tests verify the records survive install + carry the
required schema (correct view binding, LAYERS param on the WMS records,
overlay=False on the basemap).

* feat(farm_observation): geotagged field observations w/ urgency triage

New model `farm.observation` for capturing what's happening in each field:
type (pest/disease/weed/soil/water/yield/photo/other), free-text notes,
optional GeoPoint location, urgency (low/med/high), optional photo
attachment, mail.thread for chatter. Auto-named from field+type+date.

Views: list (color-coded by urgency), urgency-grouped kanban, form with
chatter + map widget on a Location tab, search with last-7-days +
high-urgency canned filters, geoengine map view that drops pins on the
field map.

Self-review fixes baked in before commit:
- `_order` would alpha-sort selection keys ("med" > "high"); fixed via a
  stored computed `urgency_rank` integer + regression test that asserts
  the actual priority order
- Dropped `widget="many2one_avatar"` on `photo_id` (the avatar widget
  is for partner-style records, not ir.attachment)

Deferred: EXIF GPS auto-population from photo uploads (needs an OWL JS
helper that parses files client-side — separate PR when a field partner
asks).

* feat(farm_water_source): wells, ponds, streams + which fields they serve

New `farm.water.source` model. Tracks the farm's water infrastructure with
its location, capacity, last-tested date, and a many2many of fields each
source serves. `geom` is `GeoMultiGeometry` so one column accepts the
shape that matches the type — point for well/trough/spring/hydrant,
linestring for stream, polygon for pond/tank.

Views: list (sum capacity), form (notebook for quality notes + location
map), search with "not tested in 1 year" canned filter, geoengine map
view showing every source on the field map.

Four tests cover all three geometry shapes (point/line/polygon) on the
same column and the field_ids many2many lookup that answers "what
fields does this source serve?".

* feat(farm_fence): fence lines + condition tracking + length compute

New `farm.fence` model. Fence type (7 options), height, condition with
mail.thread tracking, last-checked date, primary field link (nullable
for perimeter fences), and a `length_feet` auto-computed from the
polyline geometry — reprojected to EPSG:5070 Albers and converted from
meters to US survey feet to match NRCS rangeland-improvement reporting.

Self-review fix baked in:
- `_order = "condition desc"` would alpha-sort "repair > good > fair",
  hiding fair-condition fences below good ones. Added a stored computed
  `condition_rank` integer; `_order` uses that. Regression test
  asserts the actual urgency order.

Views: list (color-coded by condition), form with chatter + Fence Line
notebook tab, search with "needs repair" + "not checked in 6 months"
canned filters, geoengine map view.

Five tests: length compute (zero / from polyline / recompute on change),
condition sort regression, chatter tracking.

* refactor(farm_fence): self-review — company guard, archive, ondelete test

Self-review pass against the v1 geo slice. Three findings on farm_fence:

- Multi-company isolation wasn't enforced at write — a user in company A
  could have written a fence referencing a field in company B without
  the framework catching it. `_check_company_auto = True` on the model +
  `check_company=True` on `field_id` fixes it. (Same fix applied to
  farm_observation on its own branch.)

- No `active` field — fences get removed or replaced over time. Without
  archive support the only way to retire a fence was unlink, which loses
  history. Adds `active=True` with `boolean_toggle` widget on the form +
  an Archived filter on the search view. Mirrors farm_water_source.

- The `ondelete='set null'` behavior on `field_id` (perimeter fences
  survive their field's deletion) was an undocumented invariant. Added
  `test_field_unlink_sets_field_id_null_not_cascade` so a future ondelete
  change can't silently break it.

* fix(farm_fence): pre-empt all cascade gotchas — Alpha, shapely, pyproj, view RNG

Bundle of fixes learned across PR #7/#3/#4/#5 cascade:
- development_status=Alpha (OCA check-dev-status gate)
- Drop bogus 'code' field from farm.crop fixture
- Replace EWKT LineString strings with shapely.LineString
  (base_geoengine's GeoLineString expects shapely or EWKB hex)
- Switch reprojection from .transform() (Django-style, doesn't exist on
  shapely) to pyproj.Transformer + shapely.ops.transform — mirrors
  farm_field_geo's pattern
- Declare pyproj + shapely in external_dependencies.python
- Unwrap <group string='Group By' expand='0'> in search view
  (Odoo 19 RNG rejects both attributes on <group>)
- Replace flaky test_condition_tracking (mail.thread plumbing) with a
  field-definition probe — same approach as farm_observation

* fix(farm_fence): GeoLineString → GeoLine for base_geoengine 19.0

base_geoengine 19.0 renames the linestring field type to GeoLine
(dropped the 'String' suffix). AttributeError: module 'odoo.fields'
has no attribute 'GeoLineString'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant