Commit 12cf593
authored
feat(farm_fence): fence lines + condition tracking + length compute (#6)
* 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'.1 parent 4fb56b2 commit 12cf593
16 files changed
Lines changed: 1029 additions & 0 deletions
File tree
- farm_fence
- models
- readme
- newsfragments
- security
- static/description
- tests
- views
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
Whitespace-only changes.
0 commit comments