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
node_modules/
*.egg-info/
*.bak
*.backup
.env
*.sql
*.sql.gz
+136 -68
View File
@@ -1,82 +1,150 @@
import json
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
# doctype -> fieldname that should be auto-stamped with the logged-in worker
ACTIVITY_DOCTYPES = {
"Scouting Report": "worker",
"Order Patch": None,
"Dispatch Record": None,
"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",
# Only these may ever be created from the mobile app.
ALLOWED_DOCTYPES = {
"Scouting Report", "Spray Log Morning", "Spray Log Evening",
"Planting Record", "Soaking Record", "Tray Registry",
"Greenhouse Watering Log", "Seedling Watering Log", "Dispatch Record",
"Tray Recycling Record", "Media Mix Batch", "Cover Log",
"Customer Interaction", "Order Patch", "Resource Inventory",
}
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 submit_activity(doctype, data):
"""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()
def _assigned_zones(profile):
return [r.zone for r in (profile.get("assigned_zones") or []) if r.get("zone")]
if doctype not in ACTIVITY_DOCTYPES:
frappe.throw(f"Unknown or disallowed activity doctype: {doctype}")
if isinstance(data, str):
data = json.loads(data)
data["doctype"] = doctype
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),
)
ACTIVITY_LABEL_TO_DOCTYPE = {
"Scouting": "Scouting Report", "Order Patching": "Order Patch",
"Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch",
"Planting": "Planting Record", "Covering Seedlings": "Cover Log",
"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 _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)
def get_reference_data():
"""Dropdown/reference data the mobile app needs to populate forms
(zones, varieties, customers). Requires a valid field-worker JWT."""
get_current_field_worker()
"""Dropdown data, scoped to the caller. The app caches this for offline use."""
profile = _profile()
return {
"greenhouse_zones": frappe.get_all("Greenhouse Zone", fields=["name"]),
"seed_varieties": frappe.get_all("Seed Variety", fields=["name"]),
"customers": frappe.get_all("Customer Profile", fields=["name"]),
"zones": _assigned_zones(profile),
"activity_types": _assigned_activities(profile),
"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}
+77 -63
View File
@@ -1,72 +1,86 @@
import datetime
import json
import secrets
import frappe
import jwt
from frappe.utils import add_days, get_datetime, nowdate
from frappe.utils.password import get_decrypted_password
from datetime import datetime, timedelta
from frappe import _
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():
secret = frappe.conf.get("agri_seedling_jwt_secret")
if not secret:
frappe.throw("JWT secret not configured")
return secret
@frappe.whitelist(allow_guest=True)
def field_worker_login(worker_id, pin):
if not frappe.db.exists("Worker Profile", worker_id):
frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError)
worker = frappe.get_doc("Worker Profile", worker_id)
if not worker.active:
frappe.throw("This worker account is deactivated", frappe.AuthenticationError)
stored_pin = get_decrypted_password(
"Worker Profile", worker_id, fieldname="pin_hash", raise_exception=False
)
if not stored_pin or str(stored_pin) != str(pin):
frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError)
midnight = get_datetime(add_days(nowdate(), 1))
payload = {
"worker_id": worker.name,
"full_name": worker.full_name,
"role": "Field Worker",
"exp": midnight,
"iat": datetime.datetime.utcnow(),
ACTIVITY_LABEL_TO_DOCTYPE = {
"Scouting": "Scouting Report", "Order Patching": "Order Patch",
"Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch",
"Planting": "Planting Record", "Covering Seedlings": "Cover Log",
"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",
}
token = jwt.encode(payload, get_jwt_secret(), algorithm="HS256")
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, methods=["POST"])
def field_worker_login(worker_id, pin):
worker_id = (worker_id or "").strip()
pin = (pin or "").strip()
if not frappe.db.exists("Worker Profile", worker_id):
frappe.throw(f"Worker ID '{worker_id}' not found in system.", frappe.AuthenticationError)
# --- UNIVERSAL PIN CHECK ---
authenticated = False
# Method 1: Standard Frappe Check
try:
check_password(worker_id, pin, "Worker Profile", "pin_hash")
authenticated = True
except:
pass
# Method 2: Decryption Check (for Desk-saved PINs)
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
# Method 3: Direct DB Match (Fallback)
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
if not authenticated:
frappe.throw(f"The PIN you entered for {worker_id} is incorrect.", frappe.AuthenticationError)
profile = frappe.get_doc("Worker Profile", worker_id)
if not profile.active:
frappe.throw("This worker account is deactivated.", frappe.AuthenticationError)
assigned_activities = [
ACTIVITY_LABEL_TO_DOCTYPE.get(row.activity_type, row.activity_type)
for row in (profile.get("assigned_activity_types") or []) if row.get("activity_type")
]
token, expires_at = issue_token(worker_id)
return {
"token": token,
"worker_id": worker.name,
"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 [],
"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",
"share": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"write": 1
}
],
"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",
"name": "Customer Interaction",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 0,
"actions": [],
"autoname": "naming_series:",
"naming_series": "CI-.YYYY.-.#####",
"track_changes": 1,
"fields": [
{"fieldname": "interaction_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"},
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "in_list_view": 1},
{"fieldname": "interaction_type", "label": "Interaction Type", "fieldtype": "Select", "reqd": 1, "in_list_view": 1,
"options": "Phone Call\nFarm Visit\nEmail Inquiry\nComplaint\nOrder Inquiry"},
{"fieldname": "seedling_variety", "label": "Seedling Variety / Order of Interest", "fieldtype": "Link", "options": "Seed Variety"},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "summary", "label": "Interaction Summary", "fieldtype": "Small Text", "reqd": 1},
{"fieldname": "feedback", "label": "Customer Feedback", "fieldtype": "Select", "options": "Positive\nNeutral\nNegative"},
{"fieldname": "follow_up_required", "label": "Follow-up Required", "fieldtype": "Check"},
{"fieldname": "follow_up_task", "label": "Follow-up Task Description", "fieldtype": "Small Text", "depends_on": "follow_up_required"},
{"fieldname": "follow_up_assignee", "label": "Assignee", "fieldtype": "Link", "options": "User", "depends_on": "follow_up_required"},
{"fieldname": "next_contact_date", "label": "Next Contact Date", "fieldtype": "Date", "depends_on": "follow_up_required"},
{"fieldname": "staff_member", "label": "Staff Member", "fieldtype": "Link", "options": "User", "default": "user"}
"creation": "2026-08-05 06:22:53.359748",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"interaction_datetime",
"customer",
"interaction_type",
"seedling_variety",
"column_break_1",
"summary",
"feedback",
"follow_up_required",
"follow_up_task",
"follow_up_assignee",
"next_contact_date",
"staff_member",
"worker",
"naming_series"
],
"permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "report": 1},
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1}
]
"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": [
{
"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",
"name": "Dispatch Record",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"actions": [],
"autoname": "naming_series:",
"naming_series": "DSP-.YYYY.-.#####",
"track_changes": 1,
"fields": [
{"fieldname": "order", "label": "Order", "fieldtype": "Link", "options": "Order Patch", "reqd": 1, "in_list_view": 1},
{"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "fetch_from": "order.customer", "in_list_view": 1},
{"fieldname": "delivery_address", "label": "Delivery Address", "fieldtype": "Small Text"},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "quantity_dispatched", "label": "Quantity Dispatched (trays)", "fieldtype": "Int", "reqd": 1},
{"fieldname": "boxes_used", "label": "Boxes Used", "fieldtype": "Int"},
{"fieldname": "box_batch_number", "label": "Box Batch Number", "fieldtype": "Data"},
{"fieldname": "tape_rolls_used", "label": "Tape Rolls Used", "fieldtype": "Float"},
{"fieldname": "rope_meters_used", "label": "Rope Meters Used", "fieldtype": "Float"},
{"fieldname": "section_break_1", "label": "Transport", "fieldtype": "Section Break"},
{"fieldname": "transport_mode", "label": "Transport Mode", "fieldtype": "Select", "options": "Company Vehicle\nThird-Party Courier"},
{"fieldname": "driver_name", "label": "Driver Name", "fieldtype": "Data"},
{"fieldname": "vehicle_transport_id", "label": "Vehicle / Transport ID", "fieldtype": "Data"},
{"fieldname": "column_break_2", "fieldtype": "Column Break"},
{"fieldname": "dispatch_datetime", "label": "Dispatch Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"},
{"fieldname": "fuel_liters", "label": "Fuel Consumed (L)", "fieldtype": "Float"},
{"fieldname": "quality_check_pass", "label": "Final QC Result", "fieldtype": "Select", "options": "Pass\nFail", "reqd": 1},
{"fieldname": "quality_check_notes", "label": "QC Notes", "fieldtype": "Small Text"},
{"fieldname": "invoice_reference", "label": "Invoice Reference", "fieldtype": "Data", "read_only": 1}
"creation": "2026-08-05 06:22:58.563978",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"order",
"customer",
"delivery_address",
"column_break_1",
"quantity_dispatched",
"boxes_used",
"box_batch_number",
"tape_rolls_used",
"rope_meters_used",
"section_break_1",
"transport_mode",
"driver_name",
"vehicle_transport_id",
"column_break_2",
"dispatch_datetime",
"fuel_liters",
"quality_check_pass",
"quality_check_notes",
"invoice_reference",
"naming_series",
"amended_from"
],
"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},
{"role": "Accountant", "read": 1, "report": 1}
]
"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": [
{
"amend": 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,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-07-31 01:52:32.575812",
"modified": "2026-08-06 03:29:29.595002",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Media Mix Batch",
@@ -114,6 +114,18 @@
"role": "System 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",
@@ -13,6 +13,7 @@
"requested_delivery_date",
"greenhouse_zone",
"worker_team",
"worker",
"estimated_readiness_date",
"priority_level",
"special_handling_notes",
@@ -59,6 +60,12 @@
"label": "Worker Team",
"options": "Worker Profile"
},
{
"fieldname": "worker",
"fieldtype": "Link",
"label": "Worker",
"options": "Worker Profile"
},
{
"fieldname": "estimated_readiness_date",
"fieldtype": "Date",
@@ -103,6 +110,13 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"if_owner": 1,
"read": 1,
"role": "Field Worker",
"write": 1
}
],
"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",
"name": "Planting Record",
"module": "Agri Seedling",
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"actions": [],
"autoname": "naming_series:",
"naming_series": "PLT-.YYYY.-.#####",
"track_changes": 1,
"fields": [
{"fieldname": "planting_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"},
{"fieldname": "tray_batch_id", "label": "Tray Batch ID", "fieldtype": "Data", "reqd": 1, "in_list_view": 1},
{"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety", "reqd": 1},
{"fieldname": "seed_quantity", "label": "Seed Quantity Used", "fieldtype": "Float", "reqd": 1},
{"fieldname": "seed_lot_number", "label": "Seed Lot / Batch Number", "fieldtype": "Data", "reqd": 1},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "media_batch", "label": "Media Batch", "fieldtype": "Link", "options": "Media Mix Batch"},
{"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1},
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1},
{"fieldname": "team_size", "label": "Team Size", "fieldtype": "Int"},
{"fieldname": "section_break_1", "label": "Tray Configuration", "fieldtype": "Section Break"},
{"fieldname": "tray_count", "label": "Tray Count", "fieldtype": "Int", "reqd": 1},
{"fieldname": "cell_rows", "label": "Cell Rows", "fieldtype": "Int"},
{"fieldname": "cell_columns", "label": "Cell Columns", "fieldtype": "Int"},
{"fieldname": "seeds_per_cell", "label": "Seeds per Cell", "fieldtype": "Int"},
{"fieldname": "expected_germination_date", "label": "Expected Germination Date", "fieldtype": "Date"}
"creation": "2026-08-05 06:22:56.549409",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"planting_datetime",
"tray_batch_id",
"seedling_variety",
"seed_quantity",
"seed_lot_number",
"column_break_1",
"media_batch",
"zone",
"worker",
"team_size",
"section_break_1",
"tray_count",
"cell_rows",
"cell_columns",
"seeds_per_cell",
"expected_germination_date",
"naming_series",
"amended_from"
],
"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}
]
"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": [
{
"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",
"name": "Resource Inventory",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 0,
"actions": [],
"autoname": "field:resource_name",
"track_changes": 1,
"fields": [
{"fieldname": "resource_name", "label": "Resource Name", "fieldtype": "Data", "reqd": 1, "unique": 1, "in_list_view": 1},
{"fieldname": "category", "label": "Category", "fieldtype": "Select", "in_list_view": 1, "reqd": 1,
"options": "Labor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel"},
{"fieldname": "unit_of_measure", "label": "Unit of Measure", "fieldtype": "Select",
"options": "Person-hours\nLiters\nGrams\nCount\nkWh\nRolls\nMeters\nTrips\nKilometers"},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "current_stock", "label": "Current Stock", "fieldtype": "Float", "in_list_view": 1},
{"fieldname": "reorder_threshold", "label": "Reorder Threshold", "fieldtype": "Float",
"description": "e.g. 7-day supply equivalent for fungicides, 50 units for boxes — see Appendix C."},
{"fieldname": "reorder_quantity", "label": "Standard Reorder Quantity", "fieldtype": "Float"},
{"fieldname": "alert_priority", "label": "Alert Priority", "fieldtype": "Select", "options": "LOW\nNORMAL\nHIGH\nCRITICAL", "default": "NORMAL"},
{"fieldname": "section_break_1", "label": "Batch / Traceability", "fieldtype": "Section Break"},
{"fieldname": "batch_number", "label": "Batch / Lot Number", "fieldtype": "Data"},
{"fieldname": "supplier", "label": "Supplier", "fieldtype": "Data"},
{"fieldname": "expiry_date", "label": "Expiry Date", "fieldtype": "Date"},
{"fieldname": "registration_number", "label": "Chemical Registration Number", "fieldtype": "Data",
"description": "Required for fungicides/pesticides per Sec 9.3 compliance."},
{"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"}
"creation": "2026-08-05 06:22:52.194635",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"resource_name",
"category",
"unit_of_measure",
"column_break_1",
"current_stock",
"reorder_threshold",
"reorder_quantity",
"alert_priority",
"section_break_1",
"batch_number",
"supplier",
"expiry_date",
"registration_number",
"cost_rate",
"approval_required_role"
],
"permissions": [
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1},
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1},
{"role": "Inventory Manager", "read": 1, "write": 1, "create": 1},
{"role": "Accountant", "read": 1}
]
"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": [
{
"create": 1,
"delete": 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",
"name": "Scouting Report",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"actions": [],
"autoname": "naming_series:",
"naming_series": "SCT-.YYYY.-.#####",
"track_changes": 1,
"fields": [
{"fieldname": "inspection_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now", "in_list_view": 1},
{"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1},
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "pest_disease_type", "label": "Pest / Disease Type", "fieldtype": "Select", "in_list_view": 1,
"options": "\nAphids\nWhitefly\nSpider Mites\nDamping Off\nPowdery Mildew\nBacterial Blight\nNutrient Deficiency\nOther"},
{"fieldname": "severity", "label": "Severity (1-5)", "fieldtype": "Int", "reqd": 1, "in_list_view": 1,
"description": "1 = trace, 5 = severe outbreak. Severity >= 3 triggers a Critical push alert to Admin and Stock Manager."},
{"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety"},
{"fieldname": "percent_trays_affected", "label": "Est. % Trays Affected", "fieldtype": "Percent"},
{"fieldname": "section_break_1", "label": "Evidence", "fieldtype": "Section Break"},
{"fieldname": "photo", "label": "Photo", "fieldtype": "Attach Image",
"description": "Required (min 1) when severity >= 3 — enforced in validate()."},
{"fieldname": "gps_latitude", "label": "GPS Latitude", "fieldtype": "Float", "read_only": 1},
{"fieldname": "gps_longitude", "label": "GPS Longitude", "fieldtype": "Float", "read_only": 1},
{"fieldname": "notes", "label": "Notes / Observations", "fieldtype": "Small Text"},
{"fieldname": "section_break_2", "label": "Follow-up", "fieldtype": "Section Break"},
{"fieldname": "recommended_action", "label": "Recommended Action Flag", "fieldtype": "Small Text", "read_only": 1},
{"fieldname": "escalated", "label": "Escalated to Admin", "fieldtype": "Check", "read_only": 1, "default": "0"}
"creation": "2026-08-05 06:22:54.691986",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"inspection_datetime",
"zone",
"worker",
"column_break_1",
"pest_disease_type",
"severity",
"seedling_variety",
"percent_trays_affected",
"section_break_1",
"photo",
"gps_latitude",
"gps_longitude",
"notes",
"section_break_2",
"recommended_action",
"escalated",
"naming_series",
"amended_from"
],
"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},
{"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1}
]
"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": [
{
"amend": 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):
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:
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,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-07-31 01:41:09.884281",
"modified": "2026-08-06 03:29:29.238696",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Seed Variety",
@@ -57,6 +57,18 @@
"role": "System 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",
@@ -10,7 +10,7 @@
"date_time",
"greenhouse_zone",
"chemical_category",
"product_name",
"product",
"registration_number",
"concentration",
"volume_prepared_liters",
@@ -24,7 +24,8 @@
"morning_spray_reference",
"pest_status_change",
"weather_update",
"decontamination_confirmed"
"decontamination_confirmed",
"target_pest"
],
"fields": [
{
@@ -46,11 +47,6 @@
"label": "Chemical Category",
"options": "Fungicide\nPesticide\nFoliar"
},
{
"fieldname": "product_name",
"fieldtype": "Data",
"label": "Product Name"
},
{
"fieldname": "registration_number",
"fieldtype": "Data",
@@ -129,12 +125,24 @@
"fieldname": "decontamination_confirmed",
"fieldtype": "Check",
"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,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-07-31 01:53:08.496728",
"modified": "2026-08-06 03:21:30.352449",
"modified_by": "Administrator",
"module": "Agri Seedling",
"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",
"name": "Spray Log Morning",
"module": "Agri Seedling",
"custom": 0,
"istable": 0,
"issingle": 0,
"is_submittable": 1,
"actions": [],
"autoname": "naming_series:",
"naming_series": "SPRM-.YYYY.-.#####",
"track_changes": 1,
"allow_on_submit": [],
"fields": [
{"fieldname": "spray_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"},
{"fieldname": "zone", "label": "Greenhouse Zone / Tray Range", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1},
{"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1},
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
{"fieldname": "chemical_category", "label": "Chemical Category", "fieldtype": "Select", "reqd": 1, "in_list_view": 1,
"options": "Fungicide\nPesticide\nFoliar"},
{"fieldname": "product", "label": "Product", "fieldtype": "Link", "options": "Resource Inventory", "reqd": 1},
{"fieldname": "registration_number", "label": "Registration Number", "fieldtype": "Data", "fetch_from": "product.registration_number", "read_only": 1},
{"fieldname": "concentration", "label": "Concentration (mL/L or g/L)", "fieldtype": "Float"},
{"fieldname": "section_break_1", "label": "Volumes", "fieldtype": "Section Break"},
{"fieldname": "solution_volume_prepared", "label": "Solution Prepared (L)", "fieldtype": "Float", "reqd": 1},
{"fieldname": "solution_volume_sprayed", "label": "Solution Sprayed (L)", "fieldtype": "Float", "reqd": 1},
{"fieldname": "water_volume_used", "label": "Water Used for Dilution (L)", "fieldtype": "Float"},
{"fieldname": "target", "label": "Target (pest / deficiency)", "fieldtype": "Data"},
{"fieldname": "section_break_2", "label": "PPE Checklist", "fieldtype": "Section Break",
"description": "All four must be checked before this record can be submitted (Sec 9.3)."},
{"fieldname": "ppe_gloves", "label": "Gloves", "fieldtype": "Check"},
{"fieldname": "ppe_mask", "label": "Mask / Respirator", "fieldtype": "Check"},
{"fieldname": "column_break_2", "fieldtype": "Column Break"},
{"fieldname": "ppe_goggles", "label": "Goggles", "fieldtype": "Check"},
{"fieldname": "ppe_coveralls", "label": "Coveralls", "fieldtype": "Check"},
{"fieldname": "section_break_3", "label": "Conditions", "fieldtype": "Section Break"},
{"fieldname": "ambient_temp_c", "label": "Ambient Temp (°C)", "fieldtype": "Float"},
{"fieldname": "wind_speed", "label": "Wind Speed", "fieldtype": "Select", "options": "Calm\nLow\nModerate"},
{"fieldname": "column_break_3", "fieldtype": "Column Break"},
{"fieldname": "reentry_interval_hours", "label": "Re-Entry Interval (hrs)", "fieldtype": "Float"},
{"fieldname": "reentry_lock_expires", "label": "Zone Restricted Until", "fieldtype": "Datetime", "read_only": 1},
{"fieldname": "ppe_non_compliant", "label": "PPE Non-Compliance Flagged", "fieldtype": "Check", "read_only": 1, "default": "0"}
"creation": "2026-08-05 06:22:56.993986",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"spray_datetime",
"zone",
"worker",
"column_break_1",
"chemical_category",
"product",
"registration_number",
"concentration",
"section_break_1",
"solution_volume_prepared",
"solution_volume_sprayed",
"water_volume_used",
"target",
"section_break_2",
"ppe_gloves",
"ppe_mask",
"column_break_2",
"ppe_goggles",
"ppe_coveralls",
"section_break_3",
"ambient_temp_c",
"wind_speed",
"column_break_3",
"reentry_interval_hours",
"reentry_lock_expires",
"ppe_non_compliant",
"naming_series",
"amended_from"
],
"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}
]
"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": [
{
"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
// For license information, please see license.txt
// frappe.ui.form.on("Worker Profile", {
// refresh(frm) {
// },
// });
frappe.ui.form.on('Worker Profile', {
refresh(frm) {
if (!frm.is_new()) {
frm.add_custom_button('Generate QR Badge', () => {
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
from frappe import _
from frappe.utils import now_datetime
def deduct_spray_resources(doc, method=None):
if doc.product:
_deduct(doc.product, doc.solution_volume_sprayed or 0)
if not doc.product:
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):
if doc.product:
_deduct(doc.product, -(doc.solution_volume_sprayed or 0))
if not doc.product:
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):
seed_resource = frappe.db.get_value(
"Resource Inventory", {"category": "Seeds", "resource_name": ["like", f"%{doc.seedling_variety}%"]}, "name"
if not doc.seedling_variety:
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):
@@ -31,17 +59,34 @@ def deduct_dispatch_resources(doc, method=None):
if qty:
resource = frappe.db.get_value("Resource Inventory", {"category": category}, "name")
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:
return
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)
_log_consumption(resource_name, qty, source_doc)
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):
check_low_stock_threshold_by_name(doc.name)
@@ -1,6 +1,6 @@
{
"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",
"custom_blocks": [],
"docstatus": 0,
@@ -11,7 +11,7 @@
"is_hidden": 0,
"label": "Agri Seedling",
"links": [],
"modified": "2026-07-31 01:55:10.299652",
"modified": "2026-08-07 10:33:14.092545",
"modified_by": "Administrator",
"module": "Agri Seedling",
"name": "Agri Seedling",
@@ -123,6 +123,56 @@
"label": "Tray Recycling Record",
"link_to": "Tray Recycling Record",
"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"
+9
View File
@@ -132,6 +132,15 @@ has_permission = {
# "Field Worker" role; see agri_seedling/utils/auth.py
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 = []
+1
View File
@@ -0,0 +1 @@
qrcode[pil]
+1
View File
@@ -1 +1,2 @@
frappe
pyjwt