Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,16 @@ landa.water_body_management.StockingMeasure = class StockingMeasure extends (
) {};

cur_frm.script_manager.make(landa.water_body_management.StockingMeasure);

frappe.ui.form.on("Stocking Measure", {
setup(frm) {
frm.set_query("stocking_site", (doc, cdt, cdn) => {
return {
filters: [
["Stocking Site", "water_body", "=", doc.water_body],
["Fish Species Table", "fish_species", "=", doc.fish_species],
]
};
});
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"weight",
"weight_per_water_body_size",
"unit_of_weight_per_water_body_size",
"stocking_site",
"data_14",
"quantity",
"quantity_per_water_body_size",
Expand Down Expand Up @@ -238,16 +239,23 @@
"fieldtype": "Data",
"label": "Water Body Title",
"read_only": 1
},
{
"fieldname": "stocking_site",
"fieldtype": "Link",
"label": "Stocking Site",
"options": "Stocking Site",
"search_index": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-04 18:55:19.951039",
"modified": "2026-07-11 17:54:40.151120",
"modified_by": "Administrator",
"module": "Water Body Management",
"name": "Stocking Measure",
"naming_rule": "Expression",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# For license information, please see license.txt

import frappe
from frappe import _

from landa.water_body_management.stocking_controller import StockingController

Expand All @@ -26,6 +27,7 @@ class StockingMeasure(StockingController):
quantity: DF.Float
quantity_per_water_body_size: DF.Float
status: DF.Literal["In Progress", "Completed"]
stocking_site: DF.Link | None
Comment thread
greptile-apps[bot] marked this conversation as resolved.
stocking_target: DF.Link | None
supplier: DF.Link | None
unit_of_quantity_per_water_body_size: DF.Data | None
Expand All @@ -39,6 +41,27 @@ class StockingMeasure(StockingController):
year: DF.Int

# end: auto-generated types
def validate(self):
super().validate()
self.validate_stocking_site()

def validate_stocking_site(self):
if not self.stocking_site:
return

if frappe.db.get_value("Stocking Site", self.stocking_site, "water_body") != self.water_body:
frappe.throw(_("Stocking Site must belong to the selected Water Body."))

if not frappe.db.exists(
"Fish Species Table",
{
"parent": self.stocking_site,
"parenttype": "Stocking Site",
"fish_species": self.fish_species,
},
):
frappe.throw(_("Stocking Site does not support the selected Fish Species."))

def on_change(self):
self.update_stocking_target()

Expand Down
Empty file.
180 changes: 180 additions & 0 deletions landa/water_body_management/doctype/stocking_site/stocking_site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) 2026, ALYF GmbH and contributors
// For license information, please see license.txt

frappe.ui.form.on("Stocking Site", {
setup(frm) {
const field = frm.fields_dict.location;
field.get_leaflet_controls = () => get_marker_controls(field);
field.bind_leaflet_event_listeners = () => bind_map_events(field);
},

refresh(frm) {
frm.trigger("show_water_body");
},

water_body(frm) {
frm.trigger("show_water_body");
},

location(frm) {
frm.trigger("show_water_body");
},

show_water_body(frm) {
const field = frm.fields_dict.location;
if (!field.map) {
return;
}

remove_water_body_layer(field);
if (!frm.doc.water_body) {
return;
}

const water_body = frm.doc.water_body;
frappe
.xcall(
"landa.water_body_management.doctype.stocking_site.stocking_site.get_water_body_map_data",
{ water_body },
)
.then(({ location, margin_meters }) => {
if (frm.doc.water_body !== water_body || !location) {
return;
}

field.water_body_margin_meters = margin_meters;
field.water_body_geojson = JSON.parse(location);
field.water_body_layer = window.L.geoJSON(field.water_body_geojson, {
filter: ({ geometry }) => ["Polygon", "MultiPolygon"].includes(geometry.type),
style: ({ properties }) => ({
color: properties.is_restricted_area ? "red" : frappe.ui.color.get("blue"),
fillOpacity: 0.08,
}),
}).addTo(field.map);

const bounds = field.water_body_layer.getBounds();
if (!bounds.isValid()) {
return;
}

const max_bounds = window.L.latLngBounds(
bounds.getSouthWest().toBounds(margin_meters * 2),
).extend(bounds.getNorthEast().toBounds(margin_meters * 2));
field.map.invalidateSize();
field.map.setMaxBounds(max_bounds);
field.map.options.maxBoundsViscosity = 1;
field.map.setMinZoom(
Math.min(field.map.getBoundsZoom(max_bounds), field.map.getMaxZoom()),
);
field.map.fitBounds(bounds, { padding: [30, 30] });
});
},

validate(frm) {
get_location_point(frm.doc.location);
},
});

function get_marker_controls(field) {
return new window.L.Control.Draw({
position: "topleft",
draw: {
polyline: false,
polygon: false,
rectangle: false,
circle: false,
circlemarker: false,
marker: { repeatMode: false },
},
edit: {
featureGroup: field.editableLayers,
edit: false,
remove: true,
},
});
}

function bind_map_events(field) {
field.map.on("draw:created", async ({ layer, layerType }) => {
if (layerType !== "marker") {
return;
}
if (field.editableLayers.getLayers().length) {
frappe.msgprint(__("Only one location marker is allowed."));
return;
}

const water_body = field.frm.doc.water_body;
if (!water_body) {
frappe.msgprint(__("Select a Water Body before placing a marker."));
return;
}

const latlng = layer.getLatLng();
const is_valid = await frappe.xcall(
"landa.water_body_management.doctype.stocking_site.stocking_site.is_point_near_water_body",
{
water_body,
longitude: latlng.lng,
latitude: latlng.lat,
},
);
if (field.frm.doc.water_body !== water_body) {
return;
}
if (!is_valid) {
frappe.msgprint(
__("The location marker must be within {0} m of the Water Body.", [
field.water_body_margin_meters,
]),
);
return;
}
if (field.editableLayers.getLayers().length) {
frappe.msgprint(__("Only one location marker is allowed."));
return;
}

field.editableLayers.addLayer(layer);
set_location_value(field);
});

field.map.on("draw:deleted", () => set_location_value(field));
}

function set_location_value(field) {
field.set_value(JSON.stringify(field.editableLayers.toGeoJSON()));
}

function remove_water_body_layer(field) {
if (!field.map) {
return;
}
if (field.water_body_layer) {
field.map.removeLayer(field.water_body_layer);
delete field.water_body_layer;
}
delete field.water_body_geojson;
delete field.water_body_margin_meters;
field.map.setMaxBounds(null);
field.map.setMinZoom(0);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

function get_location_point(location) {
if (!location) {
return null;
}

const geojson = JSON.parse(location);
if (geojson.type === "FeatureCollection" && geojson.features.length === 0) {
return null;
}
if (
geojson.type !== "FeatureCollection" ||
geojson.features.length !== 1 ||
geojson.features[0].geometry.type !== "Point"
) {
frappe.throw(__("At most one location marker is allowed."));
}
return geojson.features[0].geometry.coordinates;
}
Loading
Loading