Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
dc8515b
feat(farm_base): add farm.mixin AbstractModel for color/notes/mail.th…
dnplkndll May 17, 2026
eb65779
feat(farm_crop): catalog of farm crops with 18 seeded entries
dnplkndll May 17, 2026
09c237b
feat(farm_field): fields/plots with multi-company isolation
dnplkndll May 17, 2026
7c39af9
feat(farm_planting): planting events with state machine + multi-company
dnplkndll May 17, 2026
3f30321
feat(farm_harvest): harvest events with auto-fill from planting
dnplkndll May 17, 2026
cf00cfa
fix: kanban templates for Odoo 19 Card system
dnplkndll May 17, 2026
9781760
feat(farm_field_geo): PostGIS polygon + auto-computed acreage
dnplkndll May 17, 2026
1371106
refactor(farm_field_geo): self-review fixes
dnplkndll May 17, 2026
5d3eceb
feat(farm_field_overlays): USDA Cropland + NRCS Soil + OSM overlays
dnplkndll May 17, 2026
394ca5d
feat(farm_observation): geotagged field observations w/ urgency triage
dnplkndll May 18, 2026
1debd67
feat(farm_water_source): wells, ponds, streams + which fields they serve
dnplkndll May 18, 2026
3bb2a08
feat(farm_fence): fence lines + condition tracking + length compute
dnplkndll May 18, 2026
953f350
refactor(farm_fence): self-review — company guard, archive, ondelete …
dnplkndll May 18, 2026
bb04e0c
Merge remote-tracking branch 'origin/19.0' into feat/farm-fence
dnplkndll May 18, 2026
a008d7b
fix(farm_fence): pre-empt all cascade gotchas — Alpha, shapely, pypro…
dnplkndll May 18, 2026
9b639f5
fix(farm_fence): GeoLineString → GeoLine for base_geoengine 19.0
dnplkndll May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions farm_fence/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

==========
Farm Fence
==========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:171f636929817374863dbdf0c0d452afdc46cc06088475d08a8ef94f19f96cc9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |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_fence
:alt: ledoent/farm-pack

|badge1| |badge2| |badge3|

Tracks fence lines on the farm with type (barbed wire, high-tensile,
electric, post-and-rail, woven wire, polywire, temporary), height,
condition (good / fair / needs repair), last-checked date, and an
auto-computed length in feet from the polyline geometry.

``length_feet`` mirrors the acreage compute in ``farm_field_geo``: the
WGS84 polyline is reprojected to **EPSG:5070 Conus Albers Equal Area**
and the geodesic length in meters is converted to US survey feet
(0.3048006096012192 m/ft). The number matches what NRCS would report for
the same fence line in a rangeland improvement filing.

Repairs-needed bubble to the top of every list and grouped kanban
because ``_order`` uses a stored computed ``condition_rank`` integer
mirroring the selection — a naive string DESC would put "good" above
"fair" (alpha order).

Field link is optional (``ondelete="set null"``) so a perimeter fence
spanning multiple fields keeps its history even when one field is
archived or deleted.

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Usage
=====

1. Open Farm → Fences.
2. Click **New** and pick a fence type + height.
3. Optional: link a primary field. Leave blank for perimeter fences.
4. Open the **Fence Line** tab and trace the fence on the map.
5. Save. **Length (feet)** auto-computes from the polyline.

Day-to-day:

- **Walk-the-fence rotation**: filter by "Not Checked in 6 Months" to
plan the next inspection sweep.
- **Repair triage**: kanban grouped by Condition puts Needs Repair at
the top so you can plan the materials run before the weekend.
- **Materials estimate**: open a fence record, read Length (feet),
multiply by the wire / post spec to size the order.
- **Insurance / NRCS reporting**: switch to the Map view to print a
screenshot of the entire fencing system colored by type, with each
fence's length in the legend.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/ledoent/farm-pack/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 <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**>`_.

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

Credits
=======

Authors
-------

* Ledo Enterprises

Contributors
------------

- Daniel Kendall <dkendall@ledoweb.com>

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 <https://github.com/ledoent/farm-pack/tree/19.0/farm_fence>`_ project on GitHub.

You are welcome to contribute.
1 change: 1 addition & 0 deletions farm_fence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
26 changes: 26 additions & 0 deletions farm_fence/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "Farm Fence",
"version": "19.0.1.0.0",
"summary": "Fence lines + condition tracking + auto-computed length",
"author": "Ledo Enterprises, Odoo Community Association (OCA)",
"maintainers": ["dnplkndll"],
"website": "https://github.com/ledoent/farm-pack",
"license": "AGPL-3",
"category": "Vertical/Agriculture",
# Alpha matches farm_field_geo's chain — OCA check-dev-status gate.
"development_status": "Alpha",
"depends": [
"farm_field_geo",
"mail",
],
"external_dependencies": {
"python": ["pyproj", "shapely"],
},
"data": [
"security/ir.model.access.csv",
"views/farm_fence_views.xml",
"views/farm_fence_menu.xml",
],
"installable": True,
"application": False,
}
1 change: 1 addition & 0 deletions farm_fence/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import farm_fence
127 changes: 127 additions & 0 deletions farm_fence/models/farm_fence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from functools import lru_cache

from pyproj import Transformer
from shapely.ops import transform as shapely_transform

from odoo import api, fields, models

# US survey foot in meters. Used to convert EPSG:5070 length (m) to feet
# for display — US farm fencing is universally specified in feet.
M_PER_US_SURVEY_FOOT = 0.3048006096012192

# Selection keys are descriptive strings (good/fair/repair) for the UI, but
# alpha-DESC would order "repair > good > fair" — fair-condition fences
# would hide below good-condition ones. `condition_rank` mirrors the
# selection so `_order` produces the actual urgency sort.
_CONDITION_RANK = {"good": 0, "fair": 1, "repair": 2}


@lru_cache(maxsize=1)
def _wgs84_to_albers_conus():
"""Build the EPSG:4326 → EPSG:5070 transformer once and reuse it.

Same approach farm_field_geo uses for acreage — Albers reprojection
gives a length number that matches what NRCS reports for the same
fence in a rangeland-improvement filing.
"""
return Transformer.from_crs("EPSG:4326", "EPSG:5070", always_xy=True).transform


class FarmFence(models.Model):
_name = "farm.fence"
_description = "Fence"
_inherit = ["mail.thread"]
# Repairs-needed bubble to the top.
_order = "condition_rank desc, name"
# Enforces field_id.company_id == fence.company_id at write time (only
# checked when field_id is set; perimeter fences with a null field_id
# pass through).
_check_company_auto = True

name = fields.Char(required=True, tracking=True)
active = fields.Boolean(
default=True,
help="Untick to archive fences that have been removed or replaced. "
"Their geometry and history stay on the field map's archived view.",
)
field_id = fields.Many2one(
"farm.field",
string="Primary Field",
help="Field this fence belongs to. Leave blank for perimeter fences "
"that span multiple fields.",
ondelete="set null",
check_company=True,
)
geom = fields.GeoLine(
string="Fence Line",
srid=4326,
help="Polyline tracing the fence. Length and the map view come from "
"this geometry. base_geoengine 19.0 names the type GeoLine (no "
"'String' suffix).",
)
length_feet = fields.Float(
compute="_compute_length_feet",
store=True,
digits=(10, 1),
help="Auto-computed from the polyline length, reprojected to "
"EPSG:5070 (Albers Equal Area) and converted from meters to US "
"survey feet.",
)
fence_type = fields.Selection(
[
("barbed_wire", "Barbed Wire"),
("high_tensile", "High-Tensile"),
("electric", "Electric"),
("post_rail", "Post & Rail"),
("woven_wire", "Woven Wire"),
("polywire", "Polywire (rotational)"),
("temporary", "Temporary"),
],
required=True,
tracking=True,
)
height_inches = fields.Float(digits=(5, 1))
condition = fields.Selection(
[
("good", "Good"),
("fair", "Fair"),
("repair", "Needs Repair"),
],
default="good",
required=True,
tracking=True,
)
condition_rank = fields.Integer(
compute="_compute_condition_rank",
store=True,
index=True,
help="Numeric mirror of condition so _order produces an urgency sort "
"(string DESC on selection keys would put 'good' above 'fair').",
)
last_checked_date = fields.Date(tracking=True)
notes = fields.Text()
company_id = fields.Many2one(
"res.company",
required=True,
default=lambda self: self.env.company,
index=True,
)

@api.depends("condition")
def _compute_condition_rank(self):
for rec in self:
rec.condition_rank = _CONDITION_RANK.get(rec.condition, 0)

@api.depends("geom")
def _compute_length_feet(self):
# base_geoengine returns the field as a plain shapely geometry on
# read; shapely has no .transform() method. Reproject via pyproj +
# shapely.ops.transform before measuring length. Mirrors the
# acreage compute in farm_field_geo.
transformer = _wgs84_to_albers_conus()
for rec in self:
if not rec.geom:
rec.length_feet = 0.0
continue
projected = shapely_transform(transformer, rec.geom)
rec.length_feet = projected.length / M_PER_US_SURVEY_FOOT
3 changes: 3 additions & 0 deletions farm_fence/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions farm_fence/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Daniel Kendall &lt;dkendall@ledoweb.com&gt;
19 changes: 19 additions & 0 deletions farm_fence/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Tracks fence lines on the farm with type (barbed wire, high-tensile,
electric, post-and-rail, woven wire, polywire, temporary), height,
condition (good / fair / needs repair), last-checked date, and an
auto-computed length in feet from the polyline geometry.

`length_feet` mirrors the acreage compute in `farm_field_geo`: the
WGS84 polyline is reprojected to **EPSG:5070 Conus Albers Equal Area**
and the geodesic length in meters is converted to US survey feet
(0.3048006096012192 m/ft). The number matches what NRCS would report
for the same fence line in a rangeland improvement filing.

Repairs-needed bubble to the top of every list and grouped kanban
because `_order` uses a stored computed `condition_rank` integer
mirroring the selection — a naive string DESC would put "good" above
"fair" (alpha order).

Field link is optional (`ondelete="set null"`) so a perimeter fence
spanning multiple fields keeps its history even when one field is
archived or deleted.
17 changes: 17 additions & 0 deletions farm_fence/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
1. Open Farm → Fences.
2. Click **New** and pick a fence type + height.
3. Optional: link a primary field. Leave blank for perimeter fences.
4. Open the **Fence Line** tab and trace the fence on the map.
5. Save. **Length (feet)** auto-computes from the polyline.

Day-to-day:

- **Walk-the-fence rotation**: filter by "Not Checked in 6 Months" to plan
the next inspection sweep.
- **Repair triage**: kanban grouped by Condition puts Needs Repair at the
top so you can plan the materials run before the weekend.
- **Materials estimate**: open a fence record, read Length (feet), multiply
by the wire / post spec to size the order.
- **Insurance / NRCS reporting**: switch to the Map view to print a screenshot
of the entire fencing system colored by type, with each fence's length
in the legend.
Empty file.
2 changes: 2 additions & 0 deletions farm_fence/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_farm_fence_user,farm.fence user,model_farm_fence,base.group_user,1,1,1,1
Loading
Loading