Backup current agri_seedling application

This commit is contained in:
KabundiNjuki
2026-08-27 10:59:50 +03:00
parent eea66ad833
commit 3a7e4e2dad
69 changed files with 2568 additions and 357 deletions
+5
View File
@@ -3,3 +3,8 @@ __pycache__/
.DS_Store .DS_Store
node_modules/ node_modules/
*.egg-info/ *.egg-info/
*.bak
*.backup
.env
*.sql
*.sql.gz
+138 -70
View File
@@ -1,82 +1,150 @@
import json import json
import frappe import frappe
from frappe import _
from agri_seedling.agri_seedling.auth_hooks import get_current_worker
from agri_seedling.agri_seedling.api.mobile_auth import get_current_field_worker # Only these may ever be created from the mobile app.
ALLOWED_DOCTYPES = {
# doctype -> fieldname that should be auto-stamped with the logged-in worker "Scouting Report", "Spray Log Morning", "Spray Log Evening",
ACTIVITY_DOCTYPES = { "Planting Record", "Soaking Record", "Tray Registry",
"Scouting Report": "worker", "Greenhouse Watering Log", "Seedling Watering Log", "Dispatch Record",
"Order Patch": None, "Tray Recycling Record", "Media Mix Batch", "Cover Log",
"Dispatch Record": None, "Customer Interaction", "Order Patch", "Resource Inventory",
"Media Mix Batch": "worker",
"Planting Record": "worker",
"Cover Log": "worker",
"Greenhouse Watering Log": "worker",
"Seedling Watering Log": "worker",
"Spray Log Morning": "worker",
"Spray Log Evening": "worker",
"Customer Interaction": None,
"Soaking Record": "worker",
"Tray Recycling Record": "worker",
} }
def _profile():
# This calls your hook to ensure the token is valid
return frappe.get_doc("Worker Profile", get_current_worker())
@frappe.whitelist(allow_guest=True) def _assigned_zones(profile):
def submit_activity(doctype, data): return [r.zone for r in (profile.get("assigned_zones") or []) if r.get("zone")]
"""Generic activity submission endpoint used by the React Native app
for all 13 field activities. Requires a valid field-worker JWT."""
worker = get_current_field_worker()
if doctype not in ACTIVITY_DOCTYPES: ACTIVITY_LABEL_TO_DOCTYPE = {
frappe.throw(f"Unknown or disallowed activity doctype: {doctype}") "Scouting": "Scouting Report", "Order Patching": "Order Patch",
"Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch",
if isinstance(data, str): "Planting": "Planting Record", "Covering Seedlings": "Cover Log",
data = json.loads(data) "Watering Greenhouse": "Greenhouse Watering Log", "Watering Seedlings": "Seedling Watering Log",
"Spraying Morning": "Spray Log Morning", "Spraying Evening": "Spray Log Evening",
data["doctype"] = doctype "Customer Engagement": "Customer Interaction", "Soaking": "Soaking Record",
"Recycling Trays": "Tray Recycling Record",
worker_field = ACTIVITY_DOCTYPES[doctype] }
if worker_field:
data[worker_field] = worker["worker_id"]
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)
frappe.db.commit()
return {"name": doc.name, "doctype": doctype, "status": "submitted"}
@frappe.whitelist(allow_guest=True)
def my_activities(doctype, limit=20):
"""Returns the logged-in worker's own recent submissions for a
given activity doctype (field workers can only see their own, per
the row-level permission rule in Sec 9.2 of the manual)."""
worker = get_current_field_worker()
if doctype not in ACTIVITY_DOCTYPES:
frappe.throw(f"Unknown or disallowed activity doctype: {doctype}")
worker_field = ACTIVITY_DOCTYPES[doctype]
filters = {worker_field: worker["worker_id"]} if worker_field else {}
return frappe.get_list(
doctype,
filters=filters,
fields=["name", "creation"],
order_by="creation desc",
limit_page_length=int(limit),
)
def _assigned_activities(profile):
labels = [r.activity_type for r in (profile.get("assigned_activity_types") or []) if r.get("activity_type")]
return [ACTIVITY_LABEL_TO_DOCTYPE.get(label, label) for label in labels]
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_reference_data(): def get_reference_data():
"""Dropdown/reference data the mobile app needs to populate forms """Dropdown data, scoped to the caller. The app caches this for offline use."""
(zones, varieties, customers). Requires a valid field-worker JWT.""" profile = _profile()
get_current_field_worker()
return { return {
"greenhouse_zones": frappe.get_all("Greenhouse Zone", fields=["name"]), "zones": _assigned_zones(profile),
"seed_varieties": frappe.get_all("Seed Variety", fields=["name"]), "activity_types": _assigned_activities(profile),
"customers": frappe.get_all("Customer Profile", fields=["name"]), "crops": frappe.get_all("Crop", pluck="name", limit_page_length=0)
} if frappe.db.table_exists("Crop") else [],
"pests": frappe.get_all("Pest", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Pest") else [],
"chemicals": frappe.get_all("Chemical", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Chemical") else [],
"resources": frappe.get_all("Resource Inventory", pluck="resource_name", limit_page_length=0)
if frappe.db.table_exists("Resource Inventory") else [],
"seed_varieties": frappe.get_all("Seed Variety", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Seed Variety") else [],
"tray_types": frappe.get_all("Tray Type", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Tray Type") else [],
"media_batches": frappe.get_all("Media Mix Batch", pluck="name", order_by="creation desc", limit_page_length=20)
if frappe.db.table_exists("Media Mix Batch") else [],
"customers": frappe.get_all("Customer", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Customer") else [],
"customer_profiles": frappe.get_all("Customer Profile", pluck="name", limit_page_length=0)
if frappe.db.table_exists("Customer Profile") else [],
}
@frappe.whitelist(allow_guest=True)
def submit_activity(doctype=None, data=None):
"""Server-side enforcement of assignments."""
if doctype is None or data is None:
# form_dict can arrive empty for JSON bodies in this app's request
# flow; fall back to reading the raw body directly.
try:
raw_body = frappe.local.request.get_data(as_text=True)
if raw_body:
body = json.loads(raw_body)
doctype = doctype or body.get("doctype")
data = data or body.get("data")
except Exception:
pass
if not doctype or data is None:
frappe.throw(_("Missing doctype or data in submission."), frappe.ValidationError)
worker_id = get_current_worker()
profile = frappe.get_doc("Worker Profile", worker_id)
if doctype not in ALLOWED_DOCTYPES:
frappe.throw(_("{0} cannot be submitted from the mobile app.").format(doctype), frappe.PermissionError)
if doctype not in _assigned_activities(profile):
frappe.throw(_("You are not assigned to submit {0}.").format(doctype), frappe.PermissionError)
payload = json.loads(data) if isinstance(data, str) else (data or {})
doc = frappe.new_doc(doctype)
meta = frappe.get_meta(doctype)
for key, value in payload.items():
if meta.has_field(key):
doc.set(key, value)
if meta.has_field("worker"):
doc.set("worker", worker_id)
# Belt-and-suspenders: don't rely solely on the DocType's schema-level
# "__user" default resolving correctly (that's exactly what broke
# before — a stray literal "user" string in the JSON instead of the
# magic "__user" token). Setting it explicitly here, the same way
# "worker" is set, guarantees it works regardless of DocType JSON state.
if meta.has_field("staff_member") and not doc.get("staff_member"):
doc.set("staff_member", frappe.session.user)
doc.insert(ignore_permissions=True)
frappe.db.commit()
return {"name": doc.name, "doctype": doctype}
@frappe.whitelist(allow_guest=True)
def my_activities(doctype, limit=20):
worker_id = get_current_worker()
return frappe.get_all(
doctype,
filters={"worker": worker_id},
fields=["name", "creation"],
order_by="creation desc",
limit_page_length=int(limit),
ignore_permissions=True,
)
@frappe.whitelist(allow_guest=True)
def attach_file_to_record(doctype=None, name=None, fieldname=None, file_url=None):
if doctype is None or name is None or fieldname is None or file_url is None:
try:
raw_body = frappe.local.request.get_data(as_text=True)
if raw_body:
body = json.loads(raw_body)
doctype = doctype or body.get("doctype")
name = name or body.get("name")
fieldname = fieldname or body.get("fieldname")
file_url = file_url or body.get("file_url")
except Exception:
pass
if not all([doctype, name, fieldname, file_url]):
frappe.throw(_("Missing doctype, name, fieldname or file_url."), frappe.ValidationError)
worker_id = get_current_worker()
owner_worker = frappe.db.get_value(doctype, name, "worker")
if owner_worker != worker_id:
frappe.throw(_("You can only attach files to your own records."), frappe.PermissionError)
frappe.db.set_value(doctype, name, fieldname, file_url)
frappe.db.commit()
return {"ok": True, "file_url": file_url}
+71 -57
View File
@@ -1,72 +1,86 @@
import datetime import json
import secrets
import frappe import frappe
import jwt from datetime import datetime, timedelta
from frappe.utils import add_days, get_datetime, nowdate from frappe import _
from frappe.utils.password import get_decrypted_password from frappe.utils import now_datetime
from frappe.utils.password import check_password, get_decrypted_password
TOKEN_TTL_HOURS = 24
TOKEN_CACHE_PREFIX = "worker_token"
def get_jwt_secret(): ACTIVITY_LABEL_TO_DOCTYPE = {
secret = frappe.conf.get("agri_seedling_jwt_secret") "Scouting": "Scouting Report", "Order Patching": "Order Patch",
if not secret: "Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch",
frappe.throw("JWT secret not configured") "Planting": "Planting Record", "Covering Seedlings": "Cover Log",
return secret "Watering Greenhouse": "Greenhouse Watering Log", "Watering Seedlings": "Seedling Watering Log",
"Spraying Morning": "Spray Log Morning", "Spraying Evening": "Spray Log Evening",
"Customer Engagement": "Customer Interaction", "Soaking": "Soaking Record",
"Recycling Trays": "Tray Recycling Record",
}
def issue_token(worker_id):
token = secrets.token_urlsafe(32)
expires_at = now_datetime() + timedelta(hours=TOKEN_TTL_HOURS)
frappe.cache().set_value(
f"{TOKEN_CACHE_PREFIX}:{token}",
json.dumps({"worker_id": worker_id, "expires_at": expires_at.isoformat()}),
expires_in_sec=TOKEN_TTL_HOURS * 3600,
)
return token, expires_at
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True, methods=["POST"])
def field_worker_login(worker_id, pin): def field_worker_login(worker_id, pin):
if not frappe.db.exists("Worker Profile", worker_id): worker_id = (worker_id or "").strip()
frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError) pin = (pin or "").strip()
worker = frappe.get_doc("Worker Profile", worker_id) if not frappe.db.exists("Worker Profile", worker_id):
frappe.throw(f"Worker ID '{worker_id}' not found in system.", frappe.AuthenticationError)
if not worker.active: # --- UNIVERSAL PIN CHECK ---
frappe.throw("This worker account is deactivated", frappe.AuthenticationError) authenticated = False
# Method 1: Standard Frappe Check
try:
check_password(worker_id, pin, "Worker Profile", "pin_hash")
authenticated = True
except:
pass
stored_pin = get_decrypted_password( # Method 2: Decryption Check (for Desk-saved PINs)
"Worker Profile", worker_id, fieldname="pin_hash", raise_exception=False if not authenticated:
) try:
stored = get_decrypted_password("Worker Profile", worker_id, fieldname="pin_hash")
if stored and str(stored) == str(pin):
authenticated = True
except:
pass
if not stored_pin or str(stored_pin) != str(pin): # Method 3: Direct DB Match (Fallback)
frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError) if not authenticated:
db_val = frappe.db.get_value("Worker Profile", worker_id, "pin_hash")
if db_val == pin: # Sometimes saved as plain text in dev
authenticated = True
midnight = get_datetime(add_days(nowdate(), 1)) if not authenticated:
frappe.throw(f"The PIN you entered for {worker_id} is incorrect.", frappe.AuthenticationError)
payload = { profile = frappe.get_doc("Worker Profile", worker_id)
"worker_id": worker.name, if not profile.active:
"full_name": worker.full_name, frappe.throw("This worker account is deactivated.", frappe.AuthenticationError)
"role": "Field Worker",
"exp": midnight,
"iat": datetime.datetime.utcnow(),
}
token = jwt.encode(payload, get_jwt_secret(), algorithm="HS256")
return { assigned_activities = [
"token": token, ACTIVITY_LABEL_TO_DOCTYPE.get(row.activity_type, row.activity_type)
"worker_id": worker.name, for row in (profile.get("assigned_activity_types") or []) if row.get("activity_type")
"full_name": worker.full_name, ]
"expires_at": str(midnight),
"assigned_activity_types": [row.activity_type for row in worker.assigned_activity_types],
"assigned_zones": [row.zone for row in worker.assigned_zones] if worker.assigned_zones else [],
}
token, expires_at = issue_token(worker_id)
return {
"token": token,
"expires_at": expires_at.isoformat(),
"worker_id": profile.name,
"full_name": profile.full_name,
"assigned_zones": [row.zone for row in (profile.get("assigned_zones") or []) if row.get("zone")],
"assigned_activity_types": assigned_activities
}
def get_current_field_worker():
token = frappe.get_request_header("X-Worker-Token")
if not token:
frappe.throw("Missing X-Worker-Token header", frappe.AuthenticationError)
try:
payload = jwt.decode(token, get_jwt_secret(), algorithms=["HS256"])
except jwt.ExpiredSignatureError:
frappe.throw("Session expired, please log in again", frappe.AuthenticationError)
except jwt.InvalidTokenError:
frappe.throw("Invalid session token", frappe.AuthenticationError)
return payload
@frappe.whitelist(allow_guest=True)
def whoami():
worker = get_current_field_worker()
return {"authenticated_as": worker}
+55
View File
@@ -0,0 +1,55 @@
import frappe
import json
from frappe.utils import now_datetime, get_datetime
TOKEN_CACHE_PREFIX = "worker_token"
FIELD_WORKER_PROXY_USER = "field-worker@agriseedling.local"
def authenticate_worker_token():
# Initialize the error as "Missing Token"
frappe.local.auth_error = "No X-Worker-Token found in request headers."
try:
token = frappe.get_request_header("X-Worker-Token")
if not token:
return
raw = frappe.cache().get_value(f"{TOKEN_CACHE_PREFIX}:{token}")
if not raw:
frappe.local.auth_error = "Session not found on server. You may have been logged out."
return
payload = json.loads(raw)
worker_id = payload.get("worker_id")
expires_at_str = payload.get("expires_at")
if get_datetime(expires_at_str) < now_datetime():
frappe.local.auth_error = f"Session for {worker_id} expired at {expires_at_str}."
frappe.cache().delete_value(f"{TOKEN_CACHE_PREFIX}:{token}")
return
if not frappe.db.exists("Worker Profile", worker_id):
frappe.local.auth_error = f"Worker Profile {worker_id} no longer exists."
return
if not frappe.db.exists("User", FIELD_WORKER_PROXY_USER):
frappe.local.auth_error = f"Proxy User {FIELD_WORKER_PROXY_USER} is missing. Run the console fix."
return
# Success
frappe.local.worker_id = worker_id
frappe.local.auth_error = None
frappe.logger().info(f"BEFORE set_user form_dict={dict(frappe.form_dict)}")
frappe.set_user(FIELD_WORKER_PROXY_USER)
frappe.logger().info(f"AFTER set_user form_dict={dict(frappe.form_dict)}")
except Exception as e:
frappe.local.auth_error = f"Internal Auth Error: {str(e)}"
def get_current_worker():
worker_id = getattr(frappe.local, "worker_id", None)
if not worker_id:
# Get the specific error we just found
reason = getattr(frappe.local, "auth_error", "Unknown Session Error")
frappe.throw(reason, frappe.AuthenticationError)
return worker_id
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Chemical", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,114 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:chemical_name",
"creation": "2026-08-06 03:12:41.247624",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chemical_name",
"category",
"active_ingredient",
"registration_number",
"linked_resource"
],
"fields": [
{
"fieldname": "chemical_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Chemical Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "category",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Category",
"options": "Fungicide\nPesticide\nFoliar"
},
{
"fieldname": "active_ingredient",
"fieldtype": "Data",
"label": "Active Ingredient"
},
{
"fieldname": "registration_number",
"fieldtype": "Data",
"label": "Registration Number"
},
{
"fieldname": "linked_resource",
"fieldtype": "Link",
"label": "Linked Stock Item",
"options": "Resource Inventory",
"in_list_view": 1,
"description": "The Resource Inventory record this chemical's stock is tracked under. Required for spray logs to deduct stock."
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-06 03:12:41.247624",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Chemical",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Admin",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Inventory Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Chemical(Document):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestChemical(FrappeTestCase):
pass
@@ -92,6 +92,13 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Crop", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,95 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:crop_name",
"creation": "2026-08-06 03:10:23.618416",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"crop_name",
"category",
"notes",
"linked_resource"
],
"fields": [
{
"fieldname": "crop_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Crop Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "category",
"fieldtype": "Select",
"label": "Category",
"options": "Vegetable\nHerb\nFruit\nFlower\nOther"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes"
},
{
"fieldname": "linked_resource",
"fieldtype": "Link",
"label": "Linked Seed Stock",
"options": "Resource Inventory",
"in_list_view": 1,
"description": "The Resource Inventory record this crop's seed stock is tracked under. Required for planting records to deduct stock."
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-06 03:10:23.618416",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Crop",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Admin",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Crop(Document):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCrop(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Customer", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,111 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:customer_name",
"creation": "2026-08-06 03:12:10.833021",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer_name",
"phone",
"email",
"address",
"customer_type"
],
"fields": [
{
"fieldname": "customer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Customer Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone"
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email"
},
{
"fieldname": "address",
"fieldtype": "Small Text",
"label": "Address"
},
{
"fieldname": "customer_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Customer Type",
"options": "Retail\nWholesale\nInstitutional"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-06 03:12:10.833021",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Customer",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Admin",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accountant",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Customer(Document):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCustomer(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Customer Interaction", {
// refresh(frm) {
// },
// });
@@ -1,30 +1,148 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Customer Interaction",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 0,
"autoname": "naming_series:", "autoname": "naming_series:",
"naming_series": "CI-.YYYY.-.#####", "creation": "2026-08-05 06:22:53.359748",
"track_changes": 1, "doctype": "DocType",
"fields": [ "engine": "InnoDB",
{"fieldname": "interaction_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, "field_order": [
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "in_list_view": 1}, "interaction_datetime",
{"fieldname": "interaction_type", "label": "Interaction Type", "fieldtype": "Select", "reqd": 1, "in_list_view": 1, "customer",
"options": "Phone Call\nFarm Visit\nEmail Inquiry\nComplaint\nOrder Inquiry"}, "interaction_type",
{"fieldname": "seedling_variety", "label": "Seedling Variety / Order of Interest", "fieldtype": "Link", "options": "Seed Variety"}, "seedling_variety",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "column_break_1",
{"fieldname": "summary", "label": "Interaction Summary", "fieldtype": "Small Text", "reqd": 1}, "summary",
{"fieldname": "feedback", "label": "Customer Feedback", "fieldtype": "Select", "options": "Positive\nNeutral\nNegative"}, "feedback",
{"fieldname": "follow_up_required", "label": "Follow-up Required", "fieldtype": "Check"}, "follow_up_required",
{"fieldname": "follow_up_task", "label": "Follow-up Task Description", "fieldtype": "Small Text", "depends_on": "follow_up_required"}, "follow_up_task",
{"fieldname": "follow_up_assignee", "label": "Assignee", "fieldtype": "Link", "options": "User", "depends_on": "follow_up_required"}, "follow_up_assignee",
{"fieldname": "next_contact_date", "label": "Next Contact Date", "fieldtype": "Date", "depends_on": "follow_up_required"}, "next_contact_date",
{"fieldname": "staff_member", "label": "Staff Member", "fieldtype": "Link", "options": "User", "default": "user"} "staff_member",
"worker",
"naming_series"
], ],
"fields": [
{
"default": "Now",
"fieldname": "interaction_datetime",
"fieldtype": "Datetime",
"label": "Date & Time",
"reqd": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "interaction_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Interaction Type",
"options": "Phone Call\nFarm Visit\nEmail Inquiry\nComplaint\nOrder Inquiry",
"reqd": 1
},
{
"fieldname": "seedling_variety",
"fieldtype": "Link",
"label": "Crop / Order of Interest",
"options": "Crop"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "summary",
"fieldtype": "Small Text",
"label": "Interaction Summary",
"reqd": 1
},
{
"fieldname": "feedback",
"fieldtype": "Select",
"label": "Customer Feedback",
"options": "Positive\nNeutral\nNegative"
},
{
"default": "0",
"fieldname": "follow_up_required",
"fieldtype": "Check",
"label": "Follow-up Required"
},
{
"depends_on": "follow_up_required",
"fieldname": "follow_up_task",
"fieldtype": "Small Text",
"label": "Follow-up Task Description"
},
{
"depends_on": "follow_up_required",
"fieldname": "follow_up_assignee",
"fieldtype": "Link",
"label": "Assignee",
"options": "User"
},
{
"depends_on": "follow_up_required",
"fieldname": "next_contact_date",
"fieldtype": "Date",
"label": "Next Contact Date"
},
{
"default": "__user",
"fieldname": "staff_member",
"fieldtype": "Link",
"label": "Staff Member",
"options": "User"
},
{
"fieldname": "worker",
"fieldtype": "Link",
"label": "Worker",
"options": "Worker Profile"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "CIN-.YYYY.-"
}
],
"links": [],
"modified": "2026-08-06 03:22:46.763055",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Customer Interaction",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "report": 1}, {
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1} "create": 1,
] "delete": 1,
} "read": 1,
"report": 1,
"role": "Admin",
"write": 1
},
{
"create": 1,
"read": 1,
"role": "Stock Manager",
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCustomerInteraction(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Dispatch Record", {
// refresh(frm) {
// },
// });
@@ -1,37 +1,192 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Dispatch Record",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"naming_series": "DSP-.YYYY.-.#####", "creation": "2026-08-05 06:22:58.563978",
"track_changes": 1, "doctype": "DocType",
"fields": [ "engine": "InnoDB",
{"fieldname": "order", "label": "Order", "fieldtype": "Link", "options": "Order Patch", "reqd": 1, "in_list_view": 1}, "field_order": [
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "fetch_from": "order.customer", "in_list_view": 1}, "order",
{"fieldname": "delivery_address", "label": "Delivery Address", "fieldtype": "Small Text"}, "customer",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "delivery_address",
{"fieldname": "quantity_dispatched", "label": "Quantity Dispatched (trays)", "fieldtype": "Int", "reqd": 1}, "column_break_1",
{"fieldname": "boxes_used", "label": "Boxes Used", "fieldtype": "Int"}, "quantity_dispatched",
{"fieldname": "box_batch_number", "label": "Box Batch Number", "fieldtype": "Data"}, "boxes_used",
{"fieldname": "tape_rolls_used", "label": "Tape Rolls Used", "fieldtype": "Float"}, "box_batch_number",
{"fieldname": "rope_meters_used", "label": "Rope Meters Used", "fieldtype": "Float"}, "tape_rolls_used",
{"fieldname": "section_break_1", "label": "Transport", "fieldtype": "Section Break"}, "rope_meters_used",
{"fieldname": "transport_mode", "label": "Transport Mode", "fieldtype": "Select", "options": "Company Vehicle\nThird-Party Courier"}, "section_break_1",
{"fieldname": "driver_name", "label": "Driver Name", "fieldtype": "Data"}, "transport_mode",
{"fieldname": "vehicle_transport_id", "label": "Vehicle / Transport ID", "fieldtype": "Data"}, "driver_name",
{"fieldname": "column_break_2", "fieldtype": "Column Break"}, "vehicle_transport_id",
{"fieldname": "dispatch_datetime", "label": "Dispatch Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, "column_break_2",
{"fieldname": "fuel_liters", "label": "Fuel Consumed (L)", "fieldtype": "Float"}, "dispatch_datetime",
{"fieldname": "quality_check_pass", "label": "Final QC Result", "fieldtype": "Select", "options": "Pass\nFail", "reqd": 1}, "fuel_liters",
{"fieldname": "quality_check_notes", "label": "QC Notes", "fieldtype": "Small Text"}, "quality_check_pass",
{"fieldname": "invoice_reference", "label": "Invoice Reference", "fieldtype": "Data", "read_only": 1} "quality_check_notes",
"invoice_reference",
"naming_series",
"amended_from"
], ],
"fields": [
{
"fieldname": "order",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Order",
"options": "Order Patch",
"reqd": 1
},
{
"fetch_from": "order.customer",
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "delivery_address",
"fieldtype": "Small Text",
"label": "Delivery Address"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "quantity_dispatched",
"fieldtype": "Int",
"label": "Quantity Dispatched (trays)",
"reqd": 1
},
{
"fieldname": "boxes_used",
"fieldtype": "Int",
"label": "Boxes Used"
},
{
"fieldname": "box_batch_number",
"fieldtype": "Data",
"label": "Box Batch Number"
},
{
"fieldname": "tape_rolls_used",
"fieldtype": "Float",
"label": "Tape Rolls Used"
},
{
"fieldname": "rope_meters_used",
"fieldtype": "Float",
"label": "Rope Meters Used"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Transport"
},
{
"fieldname": "transport_mode",
"fieldtype": "Select",
"label": "Transport Mode",
"options": "Company Vehicle\nThird-Party Courier"
},
{
"fieldname": "driver_name",
"fieldtype": "Data",
"label": "Driver Name"
},
{
"fieldname": "vehicle_transport_id",
"fieldtype": "Data",
"label": "Vehicle / Transport ID"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Now",
"fieldname": "dispatch_datetime",
"fieldtype": "Datetime",
"label": "Dispatch Date & Time",
"reqd": 1
},
{
"fieldname": "fuel_liters",
"fieldtype": "Float",
"label": "Fuel Consumed (L)"
},
{
"fieldname": "quality_check_pass",
"fieldtype": "Select",
"label": "Final QC Result",
"options": "Pass\nFail",
"reqd": 1
},
{
"fieldname": "quality_check_notes",
"fieldtype": "Small Text",
"label": "QC Notes"
},
{
"fieldname": "invoice_reference",
"fieldtype": "Data",
"label": "Invoice Reference",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "DSR-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Dispatch Record",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2026-08-06 03:23:16.670402",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Dispatch Record",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, {
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1, "submit": 1}, "amend": 1,
{"role": "Accountant", "read": 1, "report": 1} "cancel": 1,
] "create": 1,
} "delete": 1,
"read": 1,
"report": 1,
"role": "Admin",
"submit": 1,
"write": 1
},
{
"create": 1,
"read": 1,
"role": "Stock Manager",
"submit": 1,
"write": 1
},
{
"read": 1,
"report": 1,
"role": "Accountant"
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestDispatchRecord(FrappeTestCase):
pass
@@ -97,7 +97,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2026-07-31 01:52:32.575812", "modified": "2026-08-06 03:29:29.595002",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Agri Seedling", "module": "Agri Seedling",
"name": "Media Mix Batch", "name": "Media Mix Batch",
@@ -114,6 +114,18 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",
@@ -13,6 +13,7 @@
"requested_delivery_date", "requested_delivery_date",
"greenhouse_zone", "greenhouse_zone",
"worker_team", "worker_team",
"worker",
"estimated_readiness_date", "estimated_readiness_date",
"priority_level", "priority_level",
"special_handling_notes", "special_handling_notes",
@@ -59,6 +60,12 @@
"label": "Worker Team", "label": "Worker Team",
"options": "Worker Profile" "options": "Worker Profile"
}, },
{
"fieldname": "worker",
"fieldtype": "Link",
"label": "Worker",
"options": "Worker Profile"
},
{ {
"fieldname": "estimated_readiness_date", "fieldname": "estimated_readiness_date",
"fieldtype": "Date", "fieldtype": "Date",
@@ -103,6 +110,13 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Pest", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,87 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:pest_name",
"creation": "2026-08-06 03:11:41.797697",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"pest_name",
"pest_type",
"notes"
],
"fields": [
{
"fieldname": "pest_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Pest / Disease Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "pest_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Pest\nDisease\nNutrient Deficiency\nOther"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-06 03:11:41.797697",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Pest",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Admin",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Pest(Document):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPest(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Planting Record", {
// refresh(frm) {
// },
// });
@@ -1,33 +1,171 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Planting Record",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"naming_series": "PLT-.YYYY.-.#####", "creation": "2026-08-05 06:22:56.549409",
"track_changes": 1, "doctype": "DocType",
"fields": [ "engine": "InnoDB",
{"fieldname": "planting_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, "field_order": [
{"fieldname": "tray_batch_id", "label": "Tray Batch ID", "fieldtype": "Data", "reqd": 1, "in_list_view": 1}, "planting_datetime",
{"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety", "reqd": 1}, "tray_batch_id",
{"fieldname": "seed_quantity", "label": "Seed Quantity Used", "fieldtype": "Float", "reqd": 1}, "seedling_variety",
{"fieldname": "seed_lot_number", "label": "Seed Lot / Batch Number", "fieldtype": "Data", "reqd": 1}, "seed_quantity",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "seed_lot_number",
{"fieldname": "media_batch", "label": "Media Batch", "fieldtype": "Link", "options": "Media Mix Batch"}, "column_break_1",
{"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1}, "media_batch",
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1}, "zone",
{"fieldname": "team_size", "label": "Team Size", "fieldtype": "Int"}, "worker",
{"fieldname": "section_break_1", "label": "Tray Configuration", "fieldtype": "Section Break"}, "team_size",
{"fieldname": "tray_count", "label": "Tray Count", "fieldtype": "Int", "reqd": 1}, "section_break_1",
{"fieldname": "cell_rows", "label": "Cell Rows", "fieldtype": "Int"}, "tray_count",
{"fieldname": "cell_columns", "label": "Cell Columns", "fieldtype": "Int"}, "cell_rows",
{"fieldname": "seeds_per_cell", "label": "Seeds per Cell", "fieldtype": "Int"}, "cell_columns",
{"fieldname": "expected_germination_date", "label": "Expected Germination Date", "fieldtype": "Date"} "seeds_per_cell",
"expected_germination_date",
"naming_series",
"amended_from"
], ],
"fields": [
{
"default": "Now",
"fieldname": "planting_datetime",
"fieldtype": "Datetime",
"label": "Date & Time",
"reqd": 1
},
{
"fieldname": "tray_batch_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tray Batch ID",
"reqd": 1
},
{
"fieldname": "seedling_variety",
"fieldtype": "Link",
"label": "Crop",
"options": "Crop",
"reqd": 1
},
{
"fieldname": "seed_quantity",
"fieldtype": "Float",
"label": "Seed Quantity Used",
"reqd": 1
},
{
"fieldname": "seed_lot_number",
"fieldtype": "Data",
"label": "Seed Lot / Batch Number",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "media_batch",
"fieldtype": "Link",
"label": "Media Batch",
"options": "Media Mix Batch"
},
{
"fieldname": "zone",
"fieldtype": "Link",
"label": "Greenhouse Zone",
"options": "Greenhouse Zone",
"reqd": 1
},
{
"fieldname": "worker",
"fieldtype": "Link",
"label": "Worker",
"options": "Worker Profile",
"reqd": 1
},
{
"fieldname": "team_size",
"fieldtype": "Int",
"label": "Team Size"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Tray Configuration"
},
{
"fieldname": "tray_count",
"fieldtype": "Int",
"label": "Tray Count",
"reqd": 1
},
{
"fieldname": "cell_rows",
"fieldtype": "Int",
"label": "Cell Rows"
},
{
"fieldname": "cell_columns",
"fieldtype": "Int",
"label": "Cell Columns"
},
{
"fieldname": "seeds_per_cell",
"fieldtype": "Int",
"label": "Seeds per Cell"
},
{
"fieldname": "expected_germination_date",
"fieldtype": "Date",
"label": "Expected Germination Date"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "PLR-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Planting Record",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2026-08-06 03:22:07.512082",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Planting Record",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, {
{"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1} "amend": 1,
] "cancel": 1,
} "create": 1,
"delete": 1,
"read": 1,
"report": 1,
"role": "Admin",
"submit": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPlantingRecord(FrappeTestCase):
pass
@@ -0,0 +1,117 @@
{
"doctype": "DocType",
"name": "Resource Consumption Log",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 0,
"autoname": "hash",
"track_changes": 0,
"sort_field": "logged_on",
"sort_order": "DESC",
"fields": [
{
"fieldname": "resource",
"label": "Resource",
"fieldtype": "Link",
"options": "Resource Inventory",
"reqd": 1,
"in_list_view": 1,
"in_standard_filter": 1
},
{
"fieldname": "category",
"label": "Category",
"fieldtype": "Data",
"fetch_from": "resource.category",
"read_only": 1,
"in_list_view": 1,
"in_standard_filter": 1
},
{
"fieldname": "unit_of_measure",
"label": "Unit",
"fieldtype": "Data",
"fetch_from": "resource.unit_of_measure",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "quantity",
"label": "Quantity Used",
"fieldtype": "Float",
"reqd": 1,
"in_list_view": 1,
"description": "Positive = consumed. Negative = reversed (e.g. on cancellation)."
},
{
"fieldname": "logged_on",
"label": "Logged On",
"fieldtype": "Datetime",
"default": "Now",
"reqd": 1,
"in_list_view": 1,
"in_standard_filter": 1
},
{
"fieldname": "zone",
"label": "Zone",
"fieldtype": "Link",
"options": "Greenhouse Zone",
"in_standard_filter": 1
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
},
{
"fieldname": "source_doctype",
"label": "Source Doctype",
"fieldtype": "Link",
"options": "DocType",
"in_standard_filter": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "source_name",
"label": "Source Record",
"fieldtype": "Dynamic Link",
"options": "source_doctype"
}
],
"permissions": [
{
"role": "Admin",
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
"report": 1,
"export": 1
},
{
"role": "Stock Manager",
"read": 1,
"report": 1,
"export": 1
},
{
"role": "Inventory Manager",
"read": 1,
"report": 1,
"export": 1
},
{
"role": "Accountant",
"read": 1,
"report": 1,
"export": 1
}
]
}
@@ -0,0 +1,6 @@
import frappe
from frappe.model.document import Document
class ResourceConsumptionLog(Document):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Resource Inventory", {
// refresh(frm) {
// },
// });
@@ -1,39 +1,161 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Resource Inventory",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 0,
"autoname": "field:resource_name", "autoname": "field:resource_name",
"track_changes": 1, "creation": "2026-08-05 06:22:52.194635",
"fields": [ "doctype": "DocType",
{"fieldname": "resource_name", "label": "Resource Name", "fieldtype": "Data", "reqd": 1, "unique": 1, "in_list_view": 1}, "engine": "InnoDB",
{"fieldname": "category", "label": "Category", "fieldtype": "Select", "in_list_view": 1, "reqd": 1, "field_order": [
"options": "Labor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel"}, "resource_name",
{"fieldname": "unit_of_measure", "label": "Unit of Measure", "fieldtype": "Select", "category",
"options": "Person-hours\nLiters\nGrams\nCount\nkWh\nRolls\nMeters\nTrips\nKilometers"}, "unit_of_measure",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "column_break_1",
{"fieldname": "current_stock", "label": "Current Stock", "fieldtype": "Float", "in_list_view": 1}, "current_stock",
{"fieldname": "reorder_threshold", "label": "Reorder Threshold", "fieldtype": "Float", "reorder_threshold",
"description": "e.g. 7-day supply equivalent for fungicides, 50 units for boxes — see Appendix C."}, "reorder_quantity",
{"fieldname": "reorder_quantity", "label": "Standard Reorder Quantity", "fieldtype": "Float"}, "alert_priority",
{"fieldname": "alert_priority", "label": "Alert Priority", "fieldtype": "Select", "options": "LOW\nNORMAL\nHIGH\nCRITICAL", "default": "NORMAL"}, "section_break_1",
{"fieldname": "section_break_1", "label": "Batch / Traceability", "fieldtype": "Section Break"}, "batch_number",
{"fieldname": "batch_number", "label": "Batch / Lot Number", "fieldtype": "Data"}, "supplier",
{"fieldname": "supplier", "label": "Supplier", "fieldtype": "Data"}, "expiry_date",
{"fieldname": "expiry_date", "label": "Expiry Date", "fieldtype": "Date"}, "registration_number",
{"fieldname": "registration_number", "label": "Chemical Registration Number", "fieldtype": "Data", "cost_rate",
"description": "Required for fungicides/pesticides per Sec 9.3 compliance."}, "approval_required_role"
{"fieldname": "cost_rate", "label": "Cost Rate (per unit)", "fieldtype": "Currency"},
{"fieldname": "approval_required_role", "label": "Approval Required From", "fieldtype": "Select",
"options": "\nStock Manager\nAdmin\nAdmin + Stock Manager"}
], ],
"fields": [
{
"fieldname": "resource_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Resource Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "category",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Category",
"options": "Labor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel",
"reqd": 1
},
{
"fieldname": "unit_of_measure",
"fieldtype": "Select",
"label": "Unit of Measure",
"options": "Person-hours\nLiters\nGrams\nCount\nkWh\nRolls\nMeters\nTrips\nKilometers"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "current_stock",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Current Stock"
},
{
"description": "e.g. 7-day supply equivalent for fungicides, 50 units for boxes \u2014 see Appendix C.",
"fieldname": "reorder_threshold",
"fieldtype": "Float",
"label": "Reorder Threshold"
},
{
"fieldname": "reorder_quantity",
"fieldtype": "Float",
"label": "Standard Reorder Quantity"
},
{
"default": "NORMAL",
"fieldname": "alert_priority",
"fieldtype": "Select",
"label": "Alert Priority",
"options": "LOW\nNORMAL\nHIGH\nCRITICAL"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Batch / Traceability"
},
{
"fieldname": "batch_number",
"fieldtype": "Data",
"label": "Batch / Lot Number"
},
{
"fieldname": "supplier",
"fieldtype": "Data",
"label": "Supplier"
},
{
"fieldname": "expiry_date",
"fieldtype": "Date",
"label": "Expiry Date"
},
{
"description": "Required for fungicides/pesticides per Sec 9.3 compliance.",
"fieldname": "registration_number",
"fieldtype": "Data",
"label": "Chemical Registration Number"
},
{
"fieldname": "cost_rate",
"fieldtype": "Currency",
"label": "Cost Rate (per unit)"
},
{
"fieldname": "approval_required_role",
"fieldtype": "Select",
"label": "Approval Required From",
"options": "\nStock Manager\nAdmin\nAdmin + Stock Manager"
}
],
"links": [],
"modified": "2026-08-06 03:29:29.970439",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Resource Inventory",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1}, {
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1}, "create": 1,
{"role": "Inventory Manager", "read": 1, "write": 1, "create": 1}, "delete": 1,
{"role": "Accountant", "read": 1} "read": 1,
] "role": "Admin",
} "write": 1
},
{
"create": 1,
"read": 1,
"role": "Stock Manager",
"write": 1
},
{
"create": 1,
"read": 1,
"role": "Inventory Manager",
"write": 1
},
{
"read": 1,
"role": "Accountant"
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestResourceInventory(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Scouting Report", {
// refresh(frm) {
// },
// });
@@ -1,38 +1,186 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Scouting Report",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"naming_series": "SCT-.YYYY.-.#####", "creation": "2026-08-05 06:22:54.691986",
"track_changes": 1, "doctype": "DocType",
"fields": [ "engine": "InnoDB",
{"fieldname": "inspection_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now", "in_list_view": 1}, "field_order": [
{"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1}, "inspection_datetime",
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1}, "zone",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "worker",
{"fieldname": "pest_disease_type", "label": "Pest / Disease Type", "fieldtype": "Select", "in_list_view": 1, "column_break_1",
"options": "\nAphids\nWhitefly\nSpider Mites\nDamping Off\nPowdery Mildew\nBacterial Blight\nNutrient Deficiency\nOther"}, "pest_disease_type",
{"fieldname": "severity", "label": "Severity (1-5)", "fieldtype": "Int", "reqd": 1, "in_list_view": 1, "severity",
"description": "1 = trace, 5 = severe outbreak. Severity >= 3 triggers a Critical push alert to Admin and Stock Manager."}, "seedling_variety",
{"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety"}, "percent_trays_affected",
{"fieldname": "percent_trays_affected", "label": "Est. % Trays Affected", "fieldtype": "Percent"}, "section_break_1",
{"fieldname": "section_break_1", "label": "Evidence", "fieldtype": "Section Break"}, "photo",
{"fieldname": "photo", "label": "Photo", "fieldtype": "Attach Image", "gps_latitude",
"description": "Required (min 1) when severity >= 3 — enforced in validate()."}, "gps_longitude",
{"fieldname": "gps_latitude", "label": "GPS Latitude", "fieldtype": "Float", "read_only": 1}, "notes",
{"fieldname": "gps_longitude", "label": "GPS Longitude", "fieldtype": "Float", "read_only": 1}, "section_break_2",
{"fieldname": "notes", "label": "Notes / Observations", "fieldtype": "Small Text"}, "recommended_action",
{"fieldname": "section_break_2", "label": "Follow-up", "fieldtype": "Section Break"}, "escalated",
{"fieldname": "recommended_action", "label": "Recommended Action Flag", "fieldtype": "Small Text", "read_only": 1}, "naming_series",
{"fieldname": "escalated", "label": "Escalated to Admin", "fieldtype": "Check", "read_only": 1, "default": "0"} "amended_from"
], ],
"fields": [
{
"default": "Now",
"fieldname": "inspection_datetime",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Date & Time",
"reqd": 1
},
{
"fieldname": "zone",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Greenhouse Zone",
"options": "Greenhouse Zone",
"reqd": 1
},
{
"fieldname": "worker",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Worker",
"options": "Worker Profile",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "pest_disease_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Pest / Disease",
"options": "Pest"
},
{
"description": "1 = trace, 5 = severe outbreak. Severity >= 3 triggers a Critical push alert to Admin and Stock Manager.",
"fieldname": "severity",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Severity (1-5)",
"reqd": 1
},
{
"fieldname": "seedling_variety",
"fieldtype": "Link",
"label": "Crop",
"options": "Crop"
},
{
"fieldname": "percent_trays_affected",
"fieldtype": "Percent",
"label": "Est. % Trays Affected"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Evidence"
},
{
"description": "Required (min 1) when severity >= 3 \u2014 enforced in validate().",
"fieldname": "photo",
"fieldtype": "Attach Image",
"label": "Photo"
},
{
"fieldname": "gps_latitude",
"fieldtype": "Float",
"label": "GPS Latitude",
"read_only": 1
},
{
"fieldname": "gps_longitude",
"fieldtype": "Float",
"label": "GPS Longitude",
"read_only": 1
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes / Observations"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Follow-up"
},
{
"fieldname": "recommended_action",
"fieldtype": "Small Text",
"label": "Recommended Action Flag",
"read_only": 1
},
{
"default": "0",
"fieldname": "escalated",
"fieldtype": "Check",
"label": "Escalated to Admin",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "SCT-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Scouting Report",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2026-08-06 03:20:01.941408",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Scouting Report",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, {
{"role": "Stock Manager", "read": 1, "create": 1, "write": 1, "submit": 1, "report": 1}, "amend": 1,
{"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1} "cancel": 1,
] "create": 1,
} "delete": 1,
"read": 1,
"report": 1,
"role": "Admin",
"submit": 1,
"write": 1
},
{
"create": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"submit": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -5,6 +5,11 @@ from frappe.model.document import Document
class ScoutingReport(Document): class ScoutingReport(Document):
def validate(self): def validate(self):
# Incoming severity can arrive as a string (e.g. from mobile form
# inputs); coerce it once here so every comparison below is safe.
if self.severity not in (None, ""):
self.severity = frappe.utils.cint(self.severity)
if self.severity and self.severity >= 3 and not self.photo: if self.severity and self.severity >= 3 and not self.photo:
frappe.throw(_("At least one photo is required when severity is 3 or higher.")) frappe.throw(_("At least one photo is required when severity is 3 or higher."))
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestScoutingReport(FrappeTestCase):
pass
@@ -40,7 +40,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2026-07-31 01:41:09.884281", "modified": "2026-08-06 03:29:29.238696",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Agri Seedling", "module": "Agri Seedling",
"name": "Seed Variety", "name": "Seed Variety",
@@ -57,6 +57,18 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",
@@ -10,7 +10,7 @@
"date_time", "date_time",
"greenhouse_zone", "greenhouse_zone",
"chemical_category", "chemical_category",
"product_name", "product",
"registration_number", "registration_number",
"concentration", "concentration",
"volume_prepared_liters", "volume_prepared_liters",
@@ -24,7 +24,8 @@
"morning_spray_reference", "morning_spray_reference",
"pest_status_change", "pest_status_change",
"weather_update", "weather_update",
"decontamination_confirmed" "decontamination_confirmed",
"target_pest"
], ],
"fields": [ "fields": [
{ {
@@ -46,11 +47,6 @@
"label": "Chemical Category", "label": "Chemical Category",
"options": "Fungicide\nPesticide\nFoliar" "options": "Fungicide\nPesticide\nFoliar"
}, },
{
"fieldname": "product_name",
"fieldtype": "Data",
"label": "Product Name"
},
{ {
"fieldname": "registration_number", "fieldname": "registration_number",
"fieldtype": "Data", "fieldtype": "Data",
@@ -129,12 +125,24 @@
"fieldname": "decontamination_confirmed", "fieldname": "decontamination_confirmed",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Decontamination Confirmed" "label": "Decontamination Confirmed"
},
{
"fieldname": "product",
"fieldtype": "Link",
"label": "Chemical",
"options": "Chemical"
},
{
"fieldname": "target_pest",
"fieldtype": "Link",
"label": "Target Pest",
"options": "Pest"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2026-07-31 01:53:08.496728", "modified": "2026-08-06 03:21:30.352449",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Agri Seedling", "module": "Agri Seedling",
"name": "Spray Log Evening", "name": "Spray Log Evening",
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Spray Log Morning", {
// refresh(frm) {
// },
// });
@@ -1,47 +1,242 @@
{ {
"doctype": "DocType", "actions": [],
"name": "Spray Log Morning",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"naming_series": "SPRM-.YYYY.-.#####", "creation": "2026-08-05 06:22:56.993986",
"track_changes": 1, "doctype": "DocType",
"allow_on_submit": [], "engine": "InnoDB",
"fields": [ "field_order": [
{"fieldname": "spray_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, "spray_datetime",
{"fieldname": "zone", "label": "Greenhouse Zone / Tray Range", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1}, "zone",
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1}, "worker",
{"fieldname": "column_break_1", "fieldtype": "Column Break"}, "column_break_1",
{"fieldname": "chemical_category", "label": "Chemical Category", "fieldtype": "Select", "reqd": 1, "in_list_view": 1, "chemical_category",
"options": "Fungicide\nPesticide\nFoliar"}, "product",
{"fieldname": "product", "label": "Product", "fieldtype": "Link", "options": "Resource Inventory", "reqd": 1}, "registration_number",
{"fieldname": "registration_number", "label": "Registration Number", "fieldtype": "Data", "fetch_from": "product.registration_number", "read_only": 1}, "concentration",
{"fieldname": "concentration", "label": "Concentration (mL/L or g/L)", "fieldtype": "Float"}, "section_break_1",
{"fieldname": "section_break_1", "label": "Volumes", "fieldtype": "Section Break"}, "solution_volume_prepared",
{"fieldname": "solution_volume_prepared", "label": "Solution Prepared (L)", "fieldtype": "Float", "reqd": 1}, "solution_volume_sprayed",
{"fieldname": "solution_volume_sprayed", "label": "Solution Sprayed (L)", "fieldtype": "Float", "reqd": 1}, "water_volume_used",
{"fieldname": "water_volume_used", "label": "Water Used for Dilution (L)", "fieldtype": "Float"}, "target",
{"fieldname": "target", "label": "Target (pest / deficiency)", "fieldtype": "Data"}, "section_break_2",
{"fieldname": "section_break_2", "label": "PPE Checklist", "fieldtype": "Section Break", "ppe_gloves",
"description": "All four must be checked before this record can be submitted (Sec 9.3)."}, "ppe_mask",
{"fieldname": "ppe_gloves", "label": "Gloves", "fieldtype": "Check"}, "column_break_2",
{"fieldname": "ppe_mask", "label": "Mask / Respirator", "fieldtype": "Check"}, "ppe_goggles",
{"fieldname": "column_break_2", "fieldtype": "Column Break"}, "ppe_coveralls",
{"fieldname": "ppe_goggles", "label": "Goggles", "fieldtype": "Check"}, "section_break_3",
{"fieldname": "ppe_coveralls", "label": "Coveralls", "fieldtype": "Check"}, "ambient_temp_c",
{"fieldname": "section_break_3", "label": "Conditions", "fieldtype": "Section Break"}, "wind_speed",
{"fieldname": "ambient_temp_c", "label": "Ambient Temp (°C)", "fieldtype": "Float"}, "column_break_3",
{"fieldname": "wind_speed", "label": "Wind Speed", "fieldtype": "Select", "options": "Calm\nLow\nModerate"}, "reentry_interval_hours",
{"fieldname": "column_break_3", "fieldtype": "Column Break"}, "reentry_lock_expires",
{"fieldname": "reentry_interval_hours", "label": "Re-Entry Interval (hrs)", "fieldtype": "Float"}, "ppe_non_compliant",
{"fieldname": "reentry_lock_expires", "label": "Zone Restricted Until", "fieldtype": "Datetime", "read_only": 1}, "naming_series",
{"fieldname": "ppe_non_compliant", "label": "PPE Non-Compliance Flagged", "fieldtype": "Check", "read_only": 1, "default": "0"} "amended_from"
], ],
"fields": [
{
"default": "Now",
"fieldname": "spray_datetime",
"fieldtype": "Datetime",
"label": "Date & Time",
"reqd": 1
},
{
"fieldname": "zone",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Greenhouse Zone / Tray Range",
"options": "Greenhouse Zone",
"reqd": 1
},
{
"fieldname": "worker",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Worker",
"options": "Worker Profile",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "chemical_category",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Chemical Category",
"options": "Fungicide\nPesticide\nFoliar",
"reqd": 1
},
{
"fieldname": "product",
"fieldtype": "Link",
"label": "Chemical",
"options": "Chemical",
"reqd": 1
},
{
"fetch_from": "product.registration_number",
"fieldname": "registration_number",
"fieldtype": "Data",
"label": "Registration Number",
"read_only": 1
},
{
"fieldname": "concentration",
"fieldtype": "Float",
"label": "Concentration (mL/L or g/L)"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Volumes"
},
{
"fieldname": "solution_volume_prepared",
"fieldtype": "Float",
"label": "Solution Prepared (L)",
"reqd": 1
},
{
"fieldname": "solution_volume_sprayed",
"fieldtype": "Float",
"label": "Solution Sprayed (L)",
"reqd": 1
},
{
"fieldname": "water_volume_used",
"fieldtype": "Float",
"label": "Water Used for Dilution (L)"
},
{
"fieldname": "target",
"fieldtype": "Link",
"label": "Target Pest",
"options": "Pest"
},
{
"description": "All four must be checked before this record can be submitted (Sec 9.3).",
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "PPE Checklist"
},
{
"default": "0",
"fieldname": "ppe_gloves",
"fieldtype": "Check",
"label": "Gloves"
},
{
"default": "0",
"fieldname": "ppe_mask",
"fieldtype": "Check",
"label": "Mask / Respirator"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "ppe_goggles",
"fieldtype": "Check",
"label": "Goggles"
},
{
"default": "0",
"fieldname": "ppe_coveralls",
"fieldtype": "Check",
"label": "Coveralls"
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Conditions"
},
{
"fieldname": "ambient_temp_c",
"fieldtype": "Float",
"label": "Ambient Temp (\u00b0C)"
},
{
"fieldname": "wind_speed",
"fieldtype": "Select",
"label": "Wind Speed",
"options": "Calm\nLow\nModerate"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "reentry_interval_hours",
"fieldtype": "Float",
"label": "Re-Entry Interval (hrs)"
},
{
"fieldname": "reentry_lock_expires",
"fieldtype": "Datetime",
"label": "Zone Restricted Until",
"read_only": 1
},
{
"default": "0",
"fieldname": "ppe_non_compliant",
"fieldtype": "Check",
"label": "PPE Non-Compliance Flagged",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "SLM-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Spray Log Morning",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2026-08-06 03:20:45.930927",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Spray Log Morning",
"owner": "Administrator",
"permissions": [ "permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, {
{"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1} "amend": 1,
] "cancel": 1,
} "create": 1,
"delete": 1,
"read": 1,
"report": 1,
"role": "Admin",
"submit": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestSprayLogMorning(FrappeTestCase):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestTrayType(FrappeTestCase):
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Tray Type", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,85 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:tray_type_name",
"creation": "2026-08-06 03:13:09.593649",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tray_type_name",
"cell_count",
"material"
],
"fields": [
{
"fieldname": "tray_type_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tray Type Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "cell_count",
"fieldtype": "Int",
"label": "Cell Count"
},
{
"fieldname": "material",
"fieldtype": "Data",
"label": "Material"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-06 03:13:09.593649",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Tray Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Admin",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Field Worker",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Agri Seedling Operations Team and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TrayType(Document):
pass
@@ -1,8 +1,18 @@
// Copyright (c) 2026, Agri Seedling Operations Team and contributors frappe.ui.form.on('Worker Profile', {
// For license information, please see license.txt refresh(frm) {
if (!frm.is_new()) {
// frappe.ui.form.on("Worker Profile", { frm.add_custom_button('Generate QR Badge', () => {
// refresh(frm) { frappe.call({
method: 'agri_seedling.agri_seedling.api.worker_qr.generate_worker_qr',
// }, args: { worker_id: frm.doc.name },
// }); callback: (r) => {
if (r.message) {
frappe.msgprint('QR badge generated.');
frm.reload_doc();
}
}
});
});
}
}
});
@@ -0,0 +1,23 @@
{
"align_labels_right": 0,
"css": ".badge-card { width: 3.4in; height: 2.1in; border: 2px solid #2E7D6B; border-radius: 10px; padding: 12px 14px; font-family: 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; } .badge-header { font-size: 9px; letter-spacing: 1.5px; color: #6B7280; text-transform: uppercase; } .badge-body { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; } .badge-name { font-size: 16px; font-weight: 700; color: #1F3B57; margin: 0 0 2px 0; } .badge-id { font-size: 11px; color: #333; margin: 0; } .badge-phone { font-size: 10px; color: #6B7280; margin-top: 2px; } .badge-qr img { width: 1.15in; height: 1.15in; } .badge-footer { font-size: 8px; color: #9CA3AF; text-align: center; margin-top: 4px; }",
"custom_format": 1,
"disabled": 0,
"doc_type": "Worker Profile",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<div class=\"badge-card\">\n <div class=\"badge-header\">Agri Seedling &middot; Field Worker ID</div>\n <div class=\"badge-body\">\n <div>\n <p class=\"badge-name\">{{ doc.full_name }}</p>\n <p class=\"badge-id\">ID: {{ doc.name }}</p>\n <p class=\"badge-phone\">{{ doc.phone_number }}</p>\n </div>\n <div class=\"badge-qr\">\n {% if doc.qr_image %}\n <img src=\"{{ doc.qr_image }}\">\n {% endif %}\n </div>\n </div>\n <div class=\"badge-footer\">Scan to log field activity &mdash; do not share this badge</div>\n</div>",
"line_breaks": 0,
"margin_bottom": 5,
"margin_left": 5,
"margin_right": 5,
"margin_top": 5,
"module": "Agri Seedling",
"name": "Worker ID Badge",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"standard": "Yes"
}
@@ -0,0 +1,23 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2026-08-05 00:00:00.000000",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"module": "Agri Seedling",
"name": "Daily Resource Consumption",
"prepared_report": 0,
"ref_doctype": "Resource Inventory",
"report_name": "Daily Resource Consumption",
"report_type": "Script Report",
"roles": [
{"role": "Admin"},
{"role": "Stock Manager"},
{"role": "Inventory Manager"},
{"role": "Accountant"}
]
}
@@ -0,0 +1,42 @@
frappe.query_reports["Daily Resource Usage Summary"] = {
filters: [
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1,
},
{
fieldname: "category",
label: __("Category"),
fieldtype: "Select",
options: "\nLabor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel",
},
{
fieldname: "resource",
label: __("Resource"),
fieldtype: "Link",
options: "Resource Inventory",
},
{
fieldname: "zone",
label: __("Zone"),
fieldtype: "Link",
options: "Greenhouse Zone",
},
{
fieldname: "group_by_zone",
label: __("Break Down by Zone"),
fieldtype: "Check",
default: 0,
},
],
};
@@ -0,0 +1,23 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2026-08-07 00:00:00.000000",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"module": "Agri Seedling",
"name": "Daily Resource Usage Summary",
"prepared_report": 0,
"ref_doctype": "Resource Consumption Log",
"report_name": "Daily Resource Usage Summary",
"report_type": "Script Report",
"roles": [
{"role": "Admin"},
{"role": "Stock Manager"},
{"role": "Inventory Manager"},
{"role": "Accountant"}
]
}
@@ -0,0 +1,61 @@
import frappe
from frappe import _
from frappe.utils import getdate, today
def execute(filters=None):
filters = frappe._dict(filters or {})
if not filters.from_date:
filters.from_date = today()
if not filters.to_date:
filters.to_date = today()
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{"label": _("Resource"), "fieldname": "resource", "fieldtype": "Link", "options": "Resource Inventory", "width": 200},
{"label": _("Category"), "fieldname": "category", "fieldtype": "Data", "width": 130},
{"label": _("Total Used"), "fieldname": "total_used", "fieldtype": "Float", "width": 120},
{"label": _("Unit"), "fieldname": "unit_of_measure", "fieldtype": "Data", "width": 100},
{"label": _("Events"), "fieldname": "event_count", "fieldtype": "Int", "width": 90},
{"label": _("Zone"), "fieldname": "zone", "fieldtype": "Link", "options": "Greenhouse Zone", "width": 130},
]
def get_data(filters):
conditions = ["logged_on >= %(from_date)s", "logged_on < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)"]
values = {"from_date": getdate(filters.from_date), "to_date": getdate(filters.to_date)}
if filters.get("category"):
conditions.append("category = %(category)s")
values["category"] = filters.category
if filters.get("zone"):
conditions.append("zone = %(zone)s")
values["zone"] = filters.zone
if filters.get("resource"):
conditions.append("resource = %(resource)s")
values["resource"] = filters.resource
group_by_zone = ", zone" if filters.get("group_by_zone") else ""
select_zone = ", zone" if filters.get("group_by_zone") else ", '' as zone"
query = f"""
SELECT
resource,
category,
unit_of_measure,
SUM(quantity) AS total_used,
COUNT(*) AS event_count
{select_zone}
FROM `tabResource Consumption Log`
WHERE {" AND ".join(conditions)}
GROUP BY resource, category, unit_of_measure {group_by_zone}
ORDER BY category, total_used DESC
"""
return frappe.db.sql(query, values, as_dict=True)
+55 -10
View File
@@ -1,23 +1,51 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import now_datetime
def deduct_spray_resources(doc, method=None): def deduct_spray_resources(doc, method=None):
if doc.product: if not doc.product:
_deduct(doc.product, doc.solution_volume_sprayed or 0) return
resource = frappe.db.get_value("Chemical", doc.product, "linked_resource")
if resource:
_deduct(resource, doc.solution_volume_sprayed or 0, source_doc=doc)
else:
_warn_unlinked("Chemical", doc.product, doc)
def reverse_spray_resources(doc, method=None): def reverse_spray_resources(doc, method=None):
if doc.product: if not doc.product:
_deduct(doc.product, -(doc.solution_volume_sprayed or 0)) return
resource = frappe.db.get_value("Chemical", doc.product, "linked_resource")
if resource:
_deduct(resource, -(doc.solution_volume_sprayed or 0), source_doc=doc)
def deduct_seed_stock(doc, method=None): def deduct_seed_stock(doc, method=None):
seed_resource = frappe.db.get_value( if not doc.seedling_variety:
"Resource Inventory", {"category": "Seeds", "resource_name": ["like", f"%{doc.seedling_variety}%"]}, "name" return
resource = frappe.db.get_value("Crop", doc.seedling_variety, "linked_resource")
if resource:
_deduct(resource, doc.seed_quantity or 0, source_doc=doc)
else:
_warn_unlinked("Crop", doc.seedling_variety, doc)
def _warn_unlinked(entity_doctype, entity_name, source_doc):
"""A Chemical/Crop with no linked_resource means stock silently does not
get deducted — that used to fail completely silently. Now it alerts
Stock Manager + Admin instead, and skips the deduction."""
from agri_seedling.agri_seedling.utils.notifications import _send_push, _users_with_role
recipients = _users_with_role(["Stock Manager", "Admin"])
_send_push(
recipients,
_(
"{0} '{1}' has no linked Resource Inventory item — stock was NOT deducted for {2} {3}. "
"Set its Linked Stock Item to fix this."
).format(entity_doctype, entity_name, source_doc.doctype, source_doc.name),
priority="HIGH",
) )
if seed_resource:
_deduct(seed_resource, doc.seed_quantity or 0)
def deduct_dispatch_resources(doc, method=None): def deduct_dispatch_resources(doc, method=None):
@@ -31,17 +59,34 @@ def deduct_dispatch_resources(doc, method=None):
if qty: if qty:
resource = frappe.db.get_value("Resource Inventory", {"category": category}, "name") resource = frappe.db.get_value("Resource Inventory", {"category": category}, "name")
if resource: if resource:
_deduct(resource, qty) _deduct(resource, qty, source_doc=doc)
def _deduct(resource_name, qty): def _deduct(resource_name, qty, source_doc=None):
if not qty: if not qty:
return return
current = frappe.db.get_value("Resource Inventory", resource_name, "current_stock") or 0 current = frappe.db.get_value("Resource Inventory", resource_name, "current_stock") or 0
frappe.db.set_value("Resource Inventory", resource_name, "current_stock", current - qty) frappe.db.set_value("Resource Inventory", resource_name, "current_stock", current - qty)
_log_consumption(resource_name, qty, source_doc)
check_low_stock_threshold_by_name(resource_name) check_low_stock_threshold_by_name(resource_name)
def _log_consumption(resource_name, qty, source_doc=None):
"""Write one Resource Consumption Log row per deduction/reversal event,
so daily usage can be summed later — current_stock only tells us what's
left, not what happened."""
log = frappe.new_doc("Resource Consumption Log")
log.resource = resource_name
log.quantity = qty
log.logged_on = now_datetime()
if source_doc is not None:
log.source_doctype = source_doc.doctype
log.source_name = source_doc.name
if source_doc.meta.has_field("zone"):
log.zone = source_doc.zone
log.insert(ignore_permissions=True)
def check_low_stock_threshold(doc, method=None): def check_low_stock_threshold(doc, method=None):
check_low_stock_threshold_by_name(doc.name) check_low_stock_threshold_by_name(doc.name)
@@ -1,6 +1,6 @@
{ {
"charts": [], "charts": [],
"content": "[{\"id\": \"header1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\">Field Worker Activities</span>\", \"col\": 12}}, {\"id\": \"84af4cd220\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Scouting Report\", \"col\": 3}}, {\"id\": \"fed0636689\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Spray Log Morning\", \"col\": 3}}, {\"id\": \"3b4d8d4b24\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Planting Record\", \"col\": 3}}, {\"id\": \"387bad0415\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dispatch Record\", \"col\": 3}}, {\"id\": \"3c077375dd\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Registry\", \"col\": 3}}, {\"id\": \"26bd3c9955\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer Interaction\", \"col\": 3}}, {\"id\": \"09547223ef\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Resource Inventory\", \"col\": 3}}, {\"id\": \"d88bfef704\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Greenhouse Zone\", \"col\": 3}}, {\"id\": \"9cd8688822\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Worker Profile\", \"col\": 3}}, {\"id\": \"a1d8bfa6da\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Order Patch\", \"col\": 3}}, {\"id\": \"b56a0033c4\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Media Mix Batch\", \"col\": 3}}, {\"id\": \"ebcd178c84\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Cover Log\", \"col\": 3}}, {\"id\": \"969823d1d0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Greenhouse Watering Log\", \"col\": 3}}, {\"id\": \"cf1af5b563\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Seedling Watering Log\", \"col\": 3}}, {\"id\": \"7ef09939b5\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Spray Log Evening\", \"col\": 3}}, {\"id\": \"a21e8411df\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Soaking Record\", \"col\": 3}}, {\"id\": \"b4f78a0f7c\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Recycling Record\", \"col\": 3}}]", "content": "[{\"id\": \"header1\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\">Field Worker Activities</span>\", \"col\": 12}}, {\"id\": \"84af4cd220\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Scouting Report\", \"col\": 3}}, {\"id\": \"fed0636689\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Spray Log Morning\", \"col\": 3}}, {\"id\": \"3b4d8d4b24\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Planting Record\", \"col\": 3}}, {\"id\": \"387bad0415\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dispatch Record\", \"col\": 3}}, {\"id\": \"3c077375dd\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Registry\", \"col\": 3}}, {\"id\": \"26bd3c9955\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer Interaction\", \"col\": 3}}, {\"id\": \"09547223ef\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Resource Inventory\", \"col\": 3}}, {\"id\": \"d88bfef704\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Greenhouse Zone\", \"col\": 3}}, {\"id\": \"9cd8688822\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Worker Profile\", \"col\": 3}}, {\"id\": \"a1d8bfa6da\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Order Patch\", \"col\": 3}}, {\"id\": \"b56a0033c4\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Media Mix Batch\", \"col\": 3}}, {\"id\": \"ebcd178c84\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Cover Log\", \"col\": 3}}, {\"id\": \"969823d1d0\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Greenhouse Watering Log\", \"col\": 3}}, {\"id\": \"cf1af5b563\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Seedling Watering Log\", \"col\": 3}}, {\"id\": \"7ef09939b5\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Spray Log Evening\", \"col\": 3}}, {\"id\": \"a21e8411df\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Soaking Record\", \"col\": 3}}, {\"id\": \"b4f78a0f7c\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Recycling Record\", \"col\": 3}}, {\"id\": \"shortcut_daily_resource_consumption\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Daily Resource Consumption\", \"col\": 4}}, {\"id\": \"shortcut_resource_consumption_log\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Resource Consumption Log\", \"col\": 3}}, {\"id\": \"shortcut_daily_resource_usage_summary\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Daily Resource Usage Summary\", \"col\": 4}}, {\"id\": \"XsxJFHB6xR\", \"type\": \"header\", \"data\": {\"text\": \"<span class=\\\"h4\\\"><b></b></span>\", \"col\": 12}}, {\"id\": \"027be923d5\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Crop\", \"col\": 3}}, {\"id\": \"ccb0706ac7\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Pest\", \"col\": 3}}, {\"id\": \"92ab0d4937\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer\", \"col\": 3}}, {\"id\": \"d0c7f84e66\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chemical\", \"col\": 3}}, {\"id\": \"ab4828a739\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Type\", \"col\": 3}}]",
"creation": "2026-07-31 01:37:26.854909", "creation": "2026-07-31 01:37:26.854909",
"custom_blocks": [], "custom_blocks": [],
"docstatus": 0, "docstatus": 0,
@@ -11,7 +11,7 @@
"is_hidden": 0, "is_hidden": 0,
"label": "Agri Seedling", "label": "Agri Seedling",
"links": [], "links": [],
"modified": "2026-07-31 01:55:10.299652", "modified": "2026-08-07 10:33:14.092545",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Agri Seedling", "module": "Agri Seedling",
"name": "Agri Seedling", "name": "Agri Seedling",
@@ -123,6 +123,56 @@
"label": "Tray Recycling Record", "label": "Tray Recycling Record",
"link_to": "Tray Recycling Record", "link_to": "Tray Recycling Record",
"type": "DocType" "type": "DocType"
},
{
"doc_view": "",
"label": "Daily Resource Consumption",
"link_to": "Daily Resource Consumption",
"report_ref_doctype": "Resource Inventory",
"type": "Report"
},
{
"doc_view": "List",
"label": "Crop",
"link_to": "Crop",
"type": "DocType"
},
{
"doc_view": "List",
"label": "Pest",
"link_to": "Pest",
"type": "DocType"
},
{
"doc_view": "List",
"label": "Customer",
"link_to": "Customer",
"type": "DocType"
},
{
"doc_view": "List",
"label": "Chemical",
"link_to": "Chemical",
"type": "DocType"
},
{
"doc_view": "List",
"label": "Tray Type",
"link_to": "Tray Type",
"type": "DocType"
},
{
"doc_view": "List",
"label": "Resource Consumption Log",
"link_to": "Resource Consumption Log",
"type": "DocType"
},
{
"doc_view": "",
"label": "Daily Resource Usage Summary",
"link_to": "Daily Resource Usage Summary",
"report_ref_doctype": "Resource Consumption Log",
"type": "Report"
} }
], ],
"title": "Agri Seedling" "title": "Agri Seedling"
+9
View File
@@ -132,6 +132,15 @@ has_permission = {
# "Field Worker" role; see agri_seedling/utils/auth.py # "Field Worker" role; see agri_seedling/utils/auth.py
on_session_creation = "agri_seedling.agri_seedling.utils.auth.set_field_worker_session_expiry" on_session_creation = "agri_seedling.agri_seedling.utils.auth.set_field_worker_session_expiry"
# Before Request — X-Worker-Token auth hook for mobile field-worker app
# ---------------------------------------------------------------------------
# Validates the X-Worker-Token header on every request and upgrades the session
# from Guest to the proxy user so whitelisted API methods can execute.
before_request = [
"agri_seedling.agri_seedling.auth_hooks.authenticate_worker_token",
]
# Website Route Rules (kept minimal — mobile app is the field-worker client) # Website Route Rules (kept minimal — mobile app is the field-worker client)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
website_route_rules = [] website_route_rules = []
+1
View File
@@ -0,0 +1 @@
qrcode[pil]
+1
View File
@@ -1 +1,2 @@
frappe frappe
pyjwt