Skip to content

Commit 12cf593

Browse files
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/README.rst

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
.. image:: https://odoo-community.org/readme-banner-image
2+
:target: https://odoo-community.org/get-involved?utm_source=readme
3+
:alt: Odoo Community Association
4+
5+
==========
6+
Farm Fence
7+
==========
8+
9+
..
10+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11+
!! This file is generated by oca-gen-addon-readme !!
12+
!! changes will be overwritten. !!
13+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14+
!! source digest: sha256:171f636929817374863dbdf0c0d452afdc46cc06088475d08a8ef94f19f96cc9
15+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16+
17+
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
18+
:target: https://odoo-community.org/page/development-status
19+
:alt: Alpha
20+
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
21+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
22+
:alt: License: AGPL-3
23+
.. |badge3| image:: https://img.shields.io/badge/github-ledoent%2Ffarm--pack-lightgray.png?logo=github
24+
:target: https://github.com/ledoent/farm-pack/tree/19.0/farm_fence
25+
:alt: ledoent/farm-pack
26+
27+
|badge1| |badge2| |badge3|
28+
29+
Tracks fence lines on the farm with type (barbed wire, high-tensile,
30+
electric, post-and-rail, woven wire, polywire, temporary), height,
31+
condition (good / fair / needs repair), last-checked date, and an
32+
auto-computed length in feet from the polyline geometry.
33+
34+
``length_feet`` mirrors the acreage compute in ``farm_field_geo``: the
35+
WGS84 polyline is reprojected to **EPSG:5070 Conus Albers Equal Area**
36+
and the geodesic length in meters is converted to US survey feet
37+
(0.3048006096012192 m/ft). The number matches what NRCS would report for
38+
the same fence line in a rangeland improvement filing.
39+
40+
Repairs-needed bubble to the top of every list and grouped kanban
41+
because ``_order`` uses a stored computed ``condition_rank`` integer
42+
mirroring the selection — a naive string DESC would put "good" above
43+
"fair" (alpha order).
44+
45+
Field link is optional (``ondelete="set null"``) so a perimeter fence
46+
spanning multiple fields keeps its history even when one field is
47+
archived or deleted.
48+
49+
.. IMPORTANT::
50+
This is an alpha version, the data model and design can change at any time without warning.
51+
Only for development or testing purpose, do not use in production.
52+
`More details on development status <https://odoo-community.org/page/development-status>`_
53+
54+
**Table of contents**
55+
56+
.. contents::
57+
:local:
58+
59+
Usage
60+
=====
61+
62+
1. Open Farm → Fences.
63+
2. Click **New** and pick a fence type + height.
64+
3. Optional: link a primary field. Leave blank for perimeter fences.
65+
4. Open the **Fence Line** tab and trace the fence on the map.
66+
5. Save. **Length (feet)** auto-computes from the polyline.
67+
68+
Day-to-day:
69+
70+
- **Walk-the-fence rotation**: filter by "Not Checked in 6 Months" to
71+
plan the next inspection sweep.
72+
- **Repair triage**: kanban grouped by Condition puts Needs Repair at
73+
the top so you can plan the materials run before the weekend.
74+
- **Materials estimate**: open a fence record, read Length (feet),
75+
multiply by the wire / post spec to size the order.
76+
- **Insurance / NRCS reporting**: switch to the Map view to print a
77+
screenshot of the entire fencing system colored by type, with each
78+
fence's length in the legend.
79+
80+
Bug Tracker
81+
===========
82+
83+
Bugs are tracked on `GitHub Issues <https://github.com/ledoent/farm-pack/issues>`_.
84+
In case of trouble, please check there if your issue has already been reported.
85+
If you spotted it first, help us to smash it by providing a detailed and welcomed
86+
`feedback <https://github.com/ledoent/farm-pack/issues/new?body=module:%20farm_fence%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
87+
88+
Do not contact contributors directly about support or help with technical issues.
89+
90+
Credits
91+
=======
92+
93+
Authors
94+
-------
95+
96+
* Ledo Enterprises
97+
98+
Contributors
99+
------------
100+
101+
- Daniel Kendall <dkendall@ledoweb.com>
102+
103+
Maintainers
104+
-----------
105+
106+
.. |maintainer-dnplkndll| image:: https://github.com/dnplkndll.png?size=40px
107+
:target: https://github.com/dnplkndll
108+
:alt: dnplkndll
109+
110+
Current maintainer:
111+
112+
|maintainer-dnplkndll|
113+
114+
This module is part of the `ledoent/farm-pack <https://github.com/ledoent/farm-pack/tree/19.0/farm_fence>`_ project on GitHub.
115+
116+
You are welcome to contribute.

farm_fence/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

farm_fence/__manifest__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "Farm Fence",
3+
"version": "19.0.1.0.0",
4+
"summary": "Fence lines + condition tracking + auto-computed length",
5+
"author": "Ledo Enterprises, Odoo Community Association (OCA)",
6+
"maintainers": ["dnplkndll"],
7+
"website": "https://github.com/ledoent/farm-pack",
8+
"license": "AGPL-3",
9+
"category": "Vertical/Agriculture",
10+
# Alpha matches farm_field_geo's chain — OCA check-dev-status gate.
11+
"development_status": "Alpha",
12+
"depends": [
13+
"farm_field_geo",
14+
"mail",
15+
],
16+
"external_dependencies": {
17+
"python": ["pyproj", "shapely"],
18+
},
19+
"data": [
20+
"security/ir.model.access.csv",
21+
"views/farm_fence_views.xml",
22+
"views/farm_fence_menu.xml",
23+
],
24+
"installable": True,
25+
"application": False,
26+
}

farm_fence/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import farm_fence

farm_fence/models/farm_fence.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from functools import lru_cache
2+
3+
from pyproj import Transformer
4+
from shapely.ops import transform as shapely_transform
5+
6+
from odoo import api, fields, models
7+
8+
# US survey foot in meters. Used to convert EPSG:5070 length (m) to feet
9+
# for display — US farm fencing is universally specified in feet.
10+
M_PER_US_SURVEY_FOOT = 0.3048006096012192
11+
12+
# Selection keys are descriptive strings (good/fair/repair) for the UI, but
13+
# alpha-DESC would order "repair > good > fair" — fair-condition fences
14+
# would hide below good-condition ones. `condition_rank` mirrors the
15+
# selection so `_order` produces the actual urgency sort.
16+
_CONDITION_RANK = {"good": 0, "fair": 1, "repair": 2}
17+
18+
19+
@lru_cache(maxsize=1)
20+
def _wgs84_to_albers_conus():
21+
"""Build the EPSG:4326 → EPSG:5070 transformer once and reuse it.
22+
23+
Same approach farm_field_geo uses for acreage — Albers reprojection
24+
gives a length number that matches what NRCS reports for the same
25+
fence in a rangeland-improvement filing.
26+
"""
27+
return Transformer.from_crs("EPSG:4326", "EPSG:5070", always_xy=True).transform
28+
29+
30+
class FarmFence(models.Model):
31+
_name = "farm.fence"
32+
_description = "Fence"
33+
_inherit = ["mail.thread"]
34+
# Repairs-needed bubble to the top.
35+
_order = "condition_rank desc, name"
36+
# Enforces field_id.company_id == fence.company_id at write time (only
37+
# checked when field_id is set; perimeter fences with a null field_id
38+
# pass through).
39+
_check_company_auto = True
40+
41+
name = fields.Char(required=True, tracking=True)
42+
active = fields.Boolean(
43+
default=True,
44+
help="Untick to archive fences that have been removed or replaced. "
45+
"Their geometry and history stay on the field map's archived view.",
46+
)
47+
field_id = fields.Many2one(
48+
"farm.field",
49+
string="Primary Field",
50+
help="Field this fence belongs to. Leave blank for perimeter fences "
51+
"that span multiple fields.",
52+
ondelete="set null",
53+
check_company=True,
54+
)
55+
geom = fields.GeoLine(
56+
string="Fence Line",
57+
srid=4326,
58+
help="Polyline tracing the fence. Length and the map view come from "
59+
"this geometry. base_geoengine 19.0 names the type GeoLine (no "
60+
"'String' suffix).",
61+
)
62+
length_feet = fields.Float(
63+
compute="_compute_length_feet",
64+
store=True,
65+
digits=(10, 1),
66+
help="Auto-computed from the polyline length, reprojected to "
67+
"EPSG:5070 (Albers Equal Area) and converted from meters to US "
68+
"survey feet.",
69+
)
70+
fence_type = fields.Selection(
71+
[
72+
("barbed_wire", "Barbed Wire"),
73+
("high_tensile", "High-Tensile"),
74+
("electric", "Electric"),
75+
("post_rail", "Post & Rail"),
76+
("woven_wire", "Woven Wire"),
77+
("polywire", "Polywire (rotational)"),
78+
("temporary", "Temporary"),
79+
],
80+
required=True,
81+
tracking=True,
82+
)
83+
height_inches = fields.Float(digits=(5, 1))
84+
condition = fields.Selection(
85+
[
86+
("good", "Good"),
87+
("fair", "Fair"),
88+
("repair", "Needs Repair"),
89+
],
90+
default="good",
91+
required=True,
92+
tracking=True,
93+
)
94+
condition_rank = fields.Integer(
95+
compute="_compute_condition_rank",
96+
store=True,
97+
index=True,
98+
help="Numeric mirror of condition so _order produces an urgency sort "
99+
"(string DESC on selection keys would put 'good' above 'fair').",
100+
)
101+
last_checked_date = fields.Date(tracking=True)
102+
notes = fields.Text()
103+
company_id = fields.Many2one(
104+
"res.company",
105+
required=True,
106+
default=lambda self: self.env.company,
107+
index=True,
108+
)
109+
110+
@api.depends("condition")
111+
def _compute_condition_rank(self):
112+
for rec in self:
113+
rec.condition_rank = _CONDITION_RANK.get(rec.condition, 0)
114+
115+
@api.depends("geom")
116+
def _compute_length_feet(self):
117+
# base_geoengine returns the field as a plain shapely geometry on
118+
# read; shapely has no .transform() method. Reproject via pyproj +
119+
# shapely.ops.transform before measuring length. Mirrors the
120+
# acreage compute in farm_field_geo.
121+
transformer = _wgs84_to_albers_conus()
122+
for rec in self:
123+
if not rec.geom:
124+
rec.length_feet = 0.0
125+
continue
126+
projected = shapely_transform(transformer, rec.geom)
127+
rec.length_feet = projected.length / M_PER_US_SURVEY_FOOT

farm_fence/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"

farm_fence/readme/CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Daniel Kendall &lt;dkendall@ledoweb.com&gt;

farm_fence/readme/DESCRIPTION.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Tracks fence lines on the farm with type (barbed wire, high-tensile,
2+
electric, post-and-rail, woven wire, polywire, temporary), height,
3+
condition (good / fair / needs repair), last-checked date, and an
4+
auto-computed length in feet from the polyline geometry.
5+
6+
`length_feet` mirrors the acreage compute in `farm_field_geo`: the
7+
WGS84 polyline is reprojected to **EPSG:5070 Conus Albers Equal Area**
8+
and the geodesic length in meters is converted to US survey feet
9+
(0.3048006096012192 m/ft). The number matches what NRCS would report
10+
for the same fence line in a rangeland improvement filing.
11+
12+
Repairs-needed bubble to the top of every list and grouped kanban
13+
because `_order` uses a stored computed `condition_rank` integer
14+
mirroring the selection — a naive string DESC would put "good" above
15+
"fair" (alpha order).
16+
17+
Field link is optional (`ondelete="set null"`) so a perimeter fence
18+
spanning multiple fields keeps its history even when one field is
19+
archived or deleted.

farm_fence/readme/USAGE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
1. Open Farm → Fences.
2+
2. Click **New** and pick a fence type + height.
3+
3. Optional: link a primary field. Leave blank for perimeter fences.
4+
4. Open the **Fence Line** tab and trace the fence on the map.
5+
5. Save. **Length (feet)** auto-computes from the polyline.
6+
7+
Day-to-day:
8+
9+
- **Walk-the-fence rotation**: filter by "Not Checked in 6 Months" to plan
10+
the next inspection sweep.
11+
- **Repair triage**: kanban grouped by Condition puts Needs Repair at the
12+
top so you can plan the materials run before the weekend.
13+
- **Materials estimate**: open a fence record, read Length (feet), multiply
14+
by the wire / post spec to size the order.
15+
- **Insurance / NRCS reporting**: switch to the Map view to print a screenshot
16+
of the entire fencing system colored by type, with each fence's length
17+
in the legend.

farm_fence/readme/newsfragments/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)