Initial commit: full backend - 13 activity doctypes, JWT auth, generic activity API, QR badge generation, workspace
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
node_modules/
|
||||
*.egg-info/
|
||||
@@ -0,0 +1,3 @@
|
||||
include MANIFEST.in
|
||||
recursive-include agri_seedling *.json *.md *.txt *.html *.css *.js *.py
|
||||
recursive-exclude agri_seedling *.pyc
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
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"]),
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import datetime
|
||||
|
||||
import frappe
|
||||
import jwt
|
||||
from frappe.utils import add_days, get_datetime, nowdate
|
||||
from frappe.utils.password import get_decrypted_password
|
||||
|
||||
|
||||
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(),
|
||||
}
|
||||
token = jwt.encode(payload, get_jwt_secret(), algorithm="HS256")
|
||||
|
||||
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 [],
|
||||
}
|
||||
|
||||
|
||||
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}
|
||||
@@ -0,0 +1,50 @@
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import qrcode
|
||||
from frappe.utils.file_manager import save_file
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_worker_qr(worker_id):
|
||||
"""Admin-only. Generates (or regenerates) the QR badge for a worker,
|
||||
attaches it to their Worker Profile record, and returns the image.
|
||||
The QR intentionally does NOT contain the PIN — only the API endpoint
|
||||
and worker ID, so a lost/photographed badge alone cannot be used to
|
||||
log in."""
|
||||
if "System Manager" not in frappe.get_roles(frappe.session.user):
|
||||
frappe.throw("Not permitted", frappe.PermissionError)
|
||||
|
||||
if not frappe.db.exists("Worker Profile", worker_id):
|
||||
frappe.throw("Worker not found")
|
||||
|
||||
payload = {
|
||||
"api_url": frappe.utils.get_url(),
|
||||
"worker_id": worker_id,
|
||||
}
|
||||
token = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
|
||||
|
||||
qr_img = qrcode.make(token)
|
||||
buf = io.BytesIO()
|
||||
qr_img.save(buf, format="PNG")
|
||||
png_bytes = buf.getvalue()
|
||||
|
||||
file_doc = save_file(
|
||||
fname=f"{worker_id}_badge.png",
|
||||
content=png_bytes,
|
||||
dt="Worker Profile",
|
||||
dn=worker_id,
|
||||
is_private=0,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Worker Profile", worker_id, "qr_config_token", token)
|
||||
frappe.db.set_value("Worker Profile", worker_id, "qr_image", file_doc.file_url)
|
||||
frappe.db.commit()
|
||||
|
||||
return {
|
||||
"qr_config_token": token,
|
||||
"qr_image_base64": base64.b64encode(png_bytes).decode(),
|
||||
"file_url": file_doc.file_url,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Cover Log", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:52:42.333856",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"tray_ids",
|
||||
"cover_type",
|
||||
"greenhouse_zone",
|
||||
"worker",
|
||||
"planned_duration_days",
|
||||
"scheduled_uncovering_date",
|
||||
"cover_condition",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tray_ids",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Tray IDs"
|
||||
},
|
||||
{
|
||||
"fieldname": "cover_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Cover Type",
|
||||
"options": "Humidity Dome\nShade Cloth\nPlastic Film"
|
||||
},
|
||||
{
|
||||
"fieldname": "greenhouse_zone",
|
||||
"fieldtype": "Link",
|
||||
"label": "Greenhouse Zone",
|
||||
"options": "Greenhouse Zone"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "planned_duration_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Planned Duration (days)"
|
||||
},
|
||||
{
|
||||
"fieldname": "scheduled_uncovering_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Scheduled Uncovering Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "cover_condition",
|
||||
"fieldtype": "Select",
|
||||
"label": "Cover Condition",
|
||||
"options": "New\nReused\nDamaged"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:52:42.333856",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Cover Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 CoverLog(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 TestCoverLog(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Customer Interaction",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 0,
|
||||
"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"}
|
||||
],
|
||||
"permissions": [
|
||||
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "report": 1},
|
||||
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CustomerInteraction(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("Customer Profile", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:customer_name",
|
||||
"creation": "2026-07-31 01:42:50.210093",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_name",
|
||||
"phone",
|
||||
"email",
|
||||
"address",
|
||||
"customer_type",
|
||||
"notes"
|
||||
],
|
||||
"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",
|
||||
"label": "Customer Type",
|
||||
"options": "Retail\nWholesale\nInstitutional"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text",
|
||||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:42:50.210093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Customer Profile",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 CustomerProfile(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 TestCustomerProfile(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Dispatch Record",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 1,
|
||||
"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}
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DispatchRecord(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("Greenhouse Watering Log", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:52:50.842597",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"greenhouse_zone",
|
||||
"water_volume_liters",
|
||||
"duration_minutes",
|
||||
"watering_method",
|
||||
"worker",
|
||||
"temp_before",
|
||||
"temp_after",
|
||||
"humidity_reading",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "greenhouse_zone",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Greenhouse Zone",
|
||||
"options": "Greenhouse Zone",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "water_volume_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Water Volume (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration_minutes",
|
||||
"fieldtype": "Int",
|
||||
"label": "Duration (min)"
|
||||
},
|
||||
{
|
||||
"fieldname": "watering_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Watering Method",
|
||||
"options": "Manual Hosing\nDrip Line\nOverhead Misting"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "temp_before",
|
||||
"fieldtype": "Float",
|
||||
"label": "Temp Before (\u00b0C)"
|
||||
},
|
||||
{
|
||||
"fieldname": "temp_after",
|
||||
"fieldtype": "Float",
|
||||
"label": "Temp After (\u00b0C)"
|
||||
},
|
||||
{
|
||||
"fieldname": "humidity_reading",
|
||||
"fieldtype": "Float",
|
||||
"label": "Humidity Reading (%)"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:52:50.842597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Greenhouse Watering Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 GreenhouseWateringLog(Document):
|
||||
pass
|
||||
+9
@@ -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 TestGreenhouseWateringLog(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Greenhouse Zone",
|
||||
"module": "Agri Seedling",
|
||||
"custom": 0,
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 0,
|
||||
"autoname": "field:zone_id",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"fields": [
|
||||
{"fieldname": "zone_id", "label": "Zone ID", "fieldtype": "Data", "reqd": 1, "unique": 1, "in_list_view": 1},
|
||||
{"fieldname": "zone_name", "label": "Zone Name", "fieldtype": "Data", "in_list_view": 1},
|
||||
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
|
||||
{"fieldname": "capacity_trays", "label": "Tray Capacity", "fieldtype": "Int"},
|
||||
{"fieldname": "restricted_until", "label": "Restricted Until (REI lock)", "fieldtype": "Datetime", "read_only": 1,
|
||||
"description": "Set automatically when a spray with an active Re-Entry Interval is submitted for this zone."},
|
||||
{"fieldname": "section_break_1", "fieldtype": "Section Break"},
|
||||
{"fieldname": "owner_stock_manager", "label": "Owning Stock Manager", "fieldtype": "Link", "options": "User"},
|
||||
{"fieldname": "active", "label": "Active", "fieldtype": "Check", "default": "1"},
|
||||
{"fieldname": "notes", "label": "Notes", "fieldtype": "Small Text"}
|
||||
],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
|
||||
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1},
|
||||
{"role": "Stock Manager", "read": 1, "write": 1},
|
||||
{"role": "Inventory Manager", "read": 1},
|
||||
{"role": "Accountant", "read": 1},
|
||||
{"role": "Field Worker", "read": 1}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class GreenhouseZone(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("Media Mix Batch", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:52:32.575812",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"shift",
|
||||
"coco_peat_liters",
|
||||
"vermiculite_liters",
|
||||
"perlite_liters",
|
||||
"compost_kg",
|
||||
"total_volume_liters",
|
||||
"jigs_used",
|
||||
"mixer_equipment_id",
|
||||
"run_duration_minutes",
|
||||
"worker",
|
||||
"moisture_content",
|
||||
"texture_observation"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shift",
|
||||
"fieldtype": "Select",
|
||||
"label": "Shift",
|
||||
"options": "Morning\nAfternoon"
|
||||
},
|
||||
{
|
||||
"fieldname": "coco_peat_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Coco Peat (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "vermiculite_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Vermiculite (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "perlite_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Perlite (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "compost_kg",
|
||||
"fieldtype": "Float",
|
||||
"label": "Compost (kg)"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_volume_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Volume (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "jigs_used",
|
||||
"fieldtype": "Data",
|
||||
"label": "Jigs Used"
|
||||
},
|
||||
{
|
||||
"fieldname": "mixer_equipment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mixer Equipment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "run_duration_minutes",
|
||||
"fieldtype": "Int",
|
||||
"label": "Run Duration (min)"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "moisture_content",
|
||||
"fieldtype": "Data",
|
||||
"label": "Moisture Content"
|
||||
},
|
||||
{
|
||||
"fieldname": "texture_observation",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Texture Observation"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:52:32.575812",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Media Mix Batch",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 MediaMixBatch(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 TestMediaMixBatch(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("Order Patch", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:49:58.431409",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"seedling_variety",
|
||||
"quantity",
|
||||
"requested_delivery_date",
|
||||
"greenhouse_zone",
|
||||
"worker_team",
|
||||
"estimated_readiness_date",
|
||||
"priority_level",
|
||||
"special_handling_notes",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer",
|
||||
"options": "Customer Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "seedling_variety",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Seedling Variety",
|
||||
"options": "Seed Variety",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "requested_delivery_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Requested Delivery Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "greenhouse_zone",
|
||||
"fieldtype": "Link",
|
||||
"label": "Greenhouse Zone",
|
||||
"options": "Greenhouse Zone"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker_team",
|
||||
"fieldtype": "Link",
|
||||
"label": "Worker Team",
|
||||
"options": "Worker Profile"
|
||||
},
|
||||
{
|
||||
"fieldname": "estimated_readiness_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Estimated Readiness Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_level",
|
||||
"fieldtype": "Select",
|
||||
"label": "Priority Level",
|
||||
"options": "Standard\nExpress\nBulk"
|
||||
},
|
||||
{
|
||||
"fieldname": "special_handling_notes",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Special Handling Notes"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Pending\nIn Production\nReady for Dispatch\nDispatched"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:49:58.431409",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Order Patch",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 OrderPatch(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 TestOrderPatch(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Planting Record",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 1,
|
||||
"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"}
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PlantingRecord(Document):
|
||||
pass
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Resource Inventory",
|
||||
"module": "Agri Seedling",
|
||||
"custom": 0,
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 0,
|
||||
"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"}
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ResourceInventory(Document):
|
||||
pass
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Scouting Report",
|
||||
"module": "Agri Seedling",
|
||||
"custom": 0,
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 1,
|
||||
"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"}
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ScoutingReport(Document):
|
||||
def validate(self):
|
||||
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."))
|
||||
|
||||
if self.severity and self.severity >= 3:
|
||||
self.recommended_action = _(
|
||||
"Critical severity — isolate affected trays in {0} and notify Admin."
|
||||
).format(self.zone)
|
||||
|
||||
def on_submit(self):
|
||||
# Actual notification dispatch lives in utils/notifications.py (see hooks.py doc_events)
|
||||
if self.severity and self.severity >= 3:
|
||||
self.escalated = 1
|
||||
self.db_set("escalated", 1)
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2026, Agri Seedling Operations Team and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Seed Variety", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:variety_name",
|
||||
"creation": "2026-07-31 01:41:09.884281",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"variety_name",
|
||||
"germination_rate_benchmark",
|
||||
"days_to_germination",
|
||||
"supplier"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "variety_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Variety Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "germination_rate_benchmark",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Germination Rate Benchmark"
|
||||
},
|
||||
{
|
||||
"fieldname": "days_to_germination",
|
||||
"fieldtype": "Int",
|
||||
"label": "Days To Germination"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Data",
|
||||
"label": "Supplier"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:41:09.884281",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Seed Variety",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 SeedVariety(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 TestSeedVariety(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("Seedling Watering Log", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:53:00.645959",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"tray_ids_or_zone",
|
||||
"water_volume_liters",
|
||||
"growth_stage",
|
||||
"watering_method",
|
||||
"worker",
|
||||
"abnormalities",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tray_ids_or_zone",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Tray IDs / Zone"
|
||||
},
|
||||
{
|
||||
"fieldname": "water_volume_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Water Volume (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "growth_stage",
|
||||
"fieldtype": "Select",
|
||||
"label": "Growth Stage",
|
||||
"options": "Germination\nSeedling\nTransplant-Ready"
|
||||
},
|
||||
{
|
||||
"fieldname": "watering_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Watering Method",
|
||||
"options": "Hand Watering\nBoom Irrigator\nDrip"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "abnormalities",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Abnormalities Observed"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:53:00.645959",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Seedling Watering Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 SeedlingWateringLog(Document):
|
||||
pass
|
||||
+9
@@ -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 TestSeedlingWateringLog(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("Soaking Record", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:53:16.144036",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date_start_time",
|
||||
"seed_tray_batch_id",
|
||||
"seedling_variety",
|
||||
"seed_quantity",
|
||||
"solution_type",
|
||||
"solution_concentration",
|
||||
"total_volume_liters",
|
||||
"soak_duration_hours",
|
||||
"water_temp_c",
|
||||
"worker",
|
||||
"expected_germination_rate",
|
||||
"observations"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date_start_time",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date & Start Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "seed_tray_batch_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Seed/Tray Batch ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "seedling_variety",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Seedling Variety",
|
||||
"options": "Seed Variety",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "seed_quantity",
|
||||
"fieldtype": "Data",
|
||||
"label": "Seed Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "solution_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Soaking Solution Type",
|
||||
"options": "Plain Water\nGibberellic Acid\nHydrogen Peroxide\nOther"
|
||||
},
|
||||
{
|
||||
"fieldname": "solution_concentration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Solution Concentration"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_volume_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Volume (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "soak_duration_hours",
|
||||
"fieldtype": "Float",
|
||||
"label": "Soak Duration (hrs)"
|
||||
},
|
||||
{
|
||||
"fieldname": "water_temp_c",
|
||||
"fieldtype": "Float",
|
||||
"label": "Water Temperature (\u00b0C)"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "expected_germination_rate",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Expected Germination Rate Benchmark (%)"
|
||||
},
|
||||
{
|
||||
"fieldname": "observations",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Observations at End of Soak"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:53:16.144036",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Soaking Record",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 SoakingRecord(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 TestSoakingRecord(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("Spray Log Evening", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:53:08.496728",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date_time",
|
||||
"greenhouse_zone",
|
||||
"chemical_category",
|
||||
"product_name",
|
||||
"registration_number",
|
||||
"concentration",
|
||||
"volume_prepared_liters",
|
||||
"volume_sprayed_liters",
|
||||
"water_volume_liters",
|
||||
"worker",
|
||||
"ppe_gloves",
|
||||
"ppe_mask",
|
||||
"ppe_goggles",
|
||||
"ppe_coveralls",
|
||||
"morning_spray_reference",
|
||||
"pest_status_change",
|
||||
"weather_update",
|
||||
"decontamination_confirmed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date_time",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date & Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "greenhouse_zone",
|
||||
"fieldtype": "Link",
|
||||
"label": "Greenhouse Zone / Tray Range",
|
||||
"options": "Greenhouse Zone"
|
||||
},
|
||||
{
|
||||
"fieldname": "chemical_category",
|
||||
"fieldtype": "Select",
|
||||
"label": "Chemical Category",
|
||||
"options": "Fungicide\nPesticide\nFoliar"
|
||||
},
|
||||
{
|
||||
"fieldname": "product_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Product Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "registration_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Registration Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "concentration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Concentration"
|
||||
},
|
||||
{
|
||||
"fieldname": "volume_prepared_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Volume Prepared (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "volume_sprayed_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Volume Sprayed (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "water_volume_liters",
|
||||
"fieldtype": "Float",
|
||||
"label": "Water Volume Used (L)"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ppe_gloves",
|
||||
"fieldtype": "Check",
|
||||
"label": "PPE: Gloves"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ppe_mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "PPE: Mask/Respirator"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ppe_goggles",
|
||||
"fieldtype": "Check",
|
||||
"label": "PPE: Goggles"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ppe_coveralls",
|
||||
"fieldtype": "Check",
|
||||
"label": "PPE: Coveralls"
|
||||
},
|
||||
{
|
||||
"fieldname": "morning_spray_reference",
|
||||
"fieldtype": "Link",
|
||||
"label": "Morning Spray Reference",
|
||||
"options": "Spray Log Morning"
|
||||
},
|
||||
{
|
||||
"fieldname": "pest_status_change",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Pest/Disease Status Change"
|
||||
},
|
||||
{
|
||||
"fieldname": "weather_update",
|
||||
"fieldtype": "Data",
|
||||
"label": "Weather Condition Update"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "decontamination_confirmed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Decontamination Confirmed"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:53:08.496728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Spray Log Evening",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 SprayLogEvening(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 TestSprayLogEvening(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Spray Log Morning",
|
||||
"module": "Agri Seedling",
|
||||
"custom": 0,
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 1,
|
||||
"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"}
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
|
||||
class SprayLogMorning(Document):
|
||||
def validate(self):
|
||||
# PPE checklist is mandatory (Sec 9.3). Flag rather than silently block,
|
||||
# so Admin gets the Critical non-compliance alert instead of the record
|
||||
# just vanishing — matches the "PPE non-compliance" notification trigger.
|
||||
ppe_complete = all([self.ppe_gloves, self.ppe_mask, self.ppe_goggles, self.ppe_coveralls])
|
||||
self.ppe_non_compliant = 0 if ppe_complete else 1
|
||||
|
||||
def before_submit(self):
|
||||
if self.docstatus == 1:
|
||||
return
|
||||
if not (self.ppe_gloves and self.ppe_mask and self.ppe_goggles and self.ppe_coveralls):
|
||||
frappe.msgprint(
|
||||
_("PPE checklist is incomplete. This will be flagged to Admin for review."),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
if self.reentry_interval_hours:
|
||||
self.reentry_lock_expires = add_to_date(now_datetime(), hours=self.reentry_interval_hours)
|
||||
self.db_set("reentry_lock_expires", self.reentry_lock_expires)
|
||||
frappe.db.set_value("Greenhouse Zone", self.zone, "restricted_until", self.reentry_lock_expires)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.throw(_("Spray records are immutable once submitted. Cancellation requires Admin authorization outside the normal workflow."))
|
||||
+9
@@ -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 TestTrayRecyclingRecord(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 Recycling Record", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2026-07-31 01:53:25.349778",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"tray_batch_source",
|
||||
"total_tray_count",
|
||||
"good_count",
|
||||
"damaged_count",
|
||||
"discard_count",
|
||||
"cleaning_agent",
|
||||
"sterilization_method",
|
||||
"cleaning_solution_concentration",
|
||||
"soak_duration_minutes",
|
||||
"worker",
|
||||
"trays_returned_to_stock",
|
||||
"damaged_tray_disposal_method"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tray_batch_source",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tray Batch Source",
|
||||
"options": "Dispatch Record"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_tray_count",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Tray Count Collected"
|
||||
},
|
||||
{
|
||||
"fieldname": "good_count",
|
||||
"fieldtype": "Int",
|
||||
"label": "Good (Reusable) Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "damaged_count",
|
||||
"fieldtype": "Int",
|
||||
"label": "Damaged Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "discard_count",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discard Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "cleaning_agent",
|
||||
"fieldtype": "Data",
|
||||
"label": "Cleaning Agent Used"
|
||||
},
|
||||
{
|
||||
"fieldname": "sterilization_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Sterilization Method",
|
||||
"options": "Chemical Soak\nSteam\nUV\nBleach Wash"
|
||||
},
|
||||
{
|
||||
"fieldname": "cleaning_solution_concentration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Cleaning Solution Concentration"
|
||||
},
|
||||
{
|
||||
"fieldname": "soak_duration_minutes",
|
||||
"fieldtype": "Int",
|
||||
"label": "Soak/Sterilization Duration (min)"
|
||||
},
|
||||
{
|
||||
"fieldname": "worker",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Worker",
|
||||
"options": "Worker Profile",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "trays_returned_to_stock",
|
||||
"fieldtype": "Int",
|
||||
"label": "Trays Returned to Stock"
|
||||
},
|
||||
{
|
||||
"fieldname": "damaged_tray_disposal_method",
|
||||
"fieldtype": "Data",
|
||||
"label": "Damaged Tray Disposal Method"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-31 01:53:25.349778",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Tray Recycling Record",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System 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 TrayRecyclingRecord(Document):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Tray Registry",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 0,
|
||||
"issingle": 0,
|
||||
"is_submittable": 0,
|
||||
"autoname": "field:tray_id",
|
||||
"track_changes": 1,
|
||||
"fields": [
|
||||
{"fieldname": "tray_id", "label": "Tray ID", "fieldtype": "Data", "reqd": 1, "unique": 1, "in_list_view": 1},
|
||||
{"fieldname": "status", "label": "Status", "fieldtype": "Select", "in_list_view": 1,
|
||||
"options": "Available\nPlanted\nCovered / Germinating\nGrowing\nTransplant-Ready\nDispatched\nAwaiting Recycling\nDamaged / Discarded"},
|
||||
{"fieldname": "planting_record", "label": "Planting Record", "fieldtype": "Link", "options": "Planting Record"},
|
||||
{"fieldname": "column_break_1", "fieldtype": "Column Break"},
|
||||
{"fieldname": "zone", "label": "Current Zone", "fieldtype": "Link", "options": "Greenhouse Zone"},
|
||||
{"fieldname": "growth_stage", "label": "Growth Stage", "fieldtype": "Select",
|
||||
"options": "\nGermination\nSeedling\nTransplant-Ready"},
|
||||
{"fieldname": "last_updated", "label": "Last Status Update", "fieldtype": "Datetime", "read_only": 1}
|
||||
],
|
||||
"permissions": [
|
||||
{"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "report": 1},
|
||||
{"role": "Stock Manager", "read": 1, "write": 1, "create": 1},
|
||||
{"role": "Inventory Manager", "read": 1, "write": 1},
|
||||
{"role": "Field Worker", "read": 1, "if_owner": 1}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TrayRegistry(Document):
|
||||
pass
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Worker Activity Assignment",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 1,
|
||||
"fields": [
|
||||
{"fieldname": "activity_type", "label": "Activity Type", "fieldtype": "Select", "in_list_view": 1, "reqd": 1,
|
||||
"options": "Scouting\nOrder Patching\nOrder Dispatching\nMixing Media\nPlanting\nCovering Seedlings\nWatering Greenhouse\nWatering Seedlings\nSpraying Morning\nSpraying Evening\nCustomer Engagement\nSoaking\nRecycling Trays"}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class WorkerActivityAssignment(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 TestWorkerProfile(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("Worker Profile", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2026-07-30 17:37:40.526889",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"full_name",
|
||||
"phone_number",
|
||||
"linked_user",
|
||||
"column_break_1",
|
||||
"pin_hash",
|
||||
"qr_config_token",
|
||||
"active",
|
||||
"section_break_1",
|
||||
"assigned_zones",
|
||||
"assigned_activity_types",
|
||||
"section_break_2",
|
||||
"gps_consent",
|
||||
"onboarding_tutorial_complete",
|
||||
"first_submission_reviewed",
|
||||
"naming_series",
|
||||
"qr_image"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Full Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Number",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "linked_user",
|
||||
"fieldtype": "Link",
|
||||
"label": "Linked User (Guest)",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Stored as bcrypt hash. Never displayed or exported in plain text.",
|
||||
"fieldname": "pin_hash",
|
||||
"fieldtype": "Password",
|
||||
"label": "PIN (hashed)"
|
||||
},
|
||||
{
|
||||
"description": "Encrypted payload embedded in the printed QR card: API endpoint + worker ID + initial PIN.",
|
||||
"fieldname": "qr_config_token",
|
||||
"fieldtype": "Data",
|
||||
"label": "QR Config Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "active",
|
||||
"fieldtype": "Check",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Assignment"
|
||||
},
|
||||
{
|
||||
"fieldname": "assigned_zones",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Assigned Greenhouse Zones",
|
||||
"options": "Worker Zone Assignment"
|
||||
},
|
||||
{
|
||||
"fieldname": "assigned_activity_types",
|
||||
"fieldtype": "Table",
|
||||
"label": "Assigned Activity Types",
|
||||
"options": "Worker Activity Assignment"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consent & Compliance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "gps_consent",
|
||||
"fieldtype": "Check",
|
||||
"label": "GPS Location Consent Given"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "onboarding_tutorial_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "5-Screen Tutorial Complete"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "first_submission_reviewed",
|
||||
"fieldtype": "Check",
|
||||
"label": "First Submission Reviewed by Admin"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Naming Series",
|
||||
"options": "WRK-.#####"
|
||||
},
|
||||
{
|
||||
"fieldname": "qr_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "QR Badge",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-07-31 05:58:56.432962",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Agri Seedling",
|
||||
"name": "Worker Profile",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Admin",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "Field Worker"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class WorkerProfile(Document):
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": "Worker Zone Assignment",
|
||||
"module": "Agri Seedling",
|
||||
"istable": 1,
|
||||
"fields": [
|
||||
{"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class WorkerZoneAssignment(Document):
|
||||
pass
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def send_daily_report():
|
||||
"""Scheduled 18:00 daily — emails resource consumption summary to
|
||||
Stock Manager and Admin (Sec 8.3)."""
|
||||
from agri_seedling.agri_seedling.utils.notifications import _users_with_role
|
||||
|
||||
recipients = _users_with_role(["Stock Manager", "Admin"])
|
||||
if not recipients:
|
||||
return
|
||||
|
||||
resources = frappe.get_all(
|
||||
"Resource Inventory", fields=["resource_name", "category", "current_stock", "unit_of_measure"]
|
||||
)
|
||||
rows = "\n".join(f"{r.resource_name} ({r.category}): {r.current_stock} {r.unit_of_measure or ''}" for r in resources)
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
subject="Daily Resource Consumption Report",
|
||||
message=f"<pre>{rows}</pre>",
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
"""Query Report entry point for on-demand viewing in the web dashboard."""
|
||||
columns = [
|
||||
{"label": "Resource", "fieldname": "resource_name", "fieldtype": "Data", "width": 200},
|
||||
{"label": "Category", "fieldname": "category", "fieldtype": "Data", "width": 150},
|
||||
{"label": "Current Stock", "fieldname": "current_stock", "fieldtype": "Float", "width": 120},
|
||||
{"label": "Unit", "fieldname": "unit_of_measure", "fieldtype": "Data", "width": 100},
|
||||
]
|
||||
data = frappe.get_all(
|
||||
"Resource Inventory",
|
||||
fields=["resource_name", "category", "current_stock", "unit_of_measure"],
|
||||
order_by="category",
|
||||
)
|
||||
return columns, data
|
||||
@@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
from frappe.utils import get_datetime, nowdate, add_days
|
||||
|
||||
|
||||
def set_field_worker_session_expiry(login_manager=None):
|
||||
"""Field Worker sessions expire at midnight (Sec 9.1). Other roles use
|
||||
the standard 8-hour token expiry configured in site_config."""
|
||||
user = frappe.session.user
|
||||
if user == "Administrator":
|
||||
return
|
||||
if "Field Worker" not in frappe.get_roles(user):
|
||||
return
|
||||
midnight = get_datetime(add_days(nowdate(), 1))
|
||||
frappe.db.set_value(
|
||||
"Sessions",
|
||||
{"user": user, "sid": frappe.session.sid},
|
||||
"lastupdate",
|
||||
midnight,
|
||||
update_modified=False,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
import frappe
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
|
||||
def enforce_ppe_checklist(doc, method=None):
|
||||
ppe_complete = all([doc.ppe_gloves, doc.ppe_mask, doc.ppe_goggles, doc.ppe_coveralls])
|
||||
doc.ppe_non_compliant = 0 if ppe_complete else 1
|
||||
|
||||
|
||||
def start_reentry_interval_lock(doc, method=None):
|
||||
from frappe.utils import add_to_date
|
||||
|
||||
if doc.reentry_interval_hours and doc.zone:
|
||||
expiry = add_to_date(now_datetime(), hours=doc.reentry_interval_hours)
|
||||
frappe.db.set_value("Greenhouse Zone", doc.zone, "restricted_until", expiry)
|
||||
|
||||
if doc.ppe_non_compliant:
|
||||
from agri_seedling.agri_seedling.utils.notifications import _send_push, _users_with_role
|
||||
|
||||
recipients = _users_with_role(["Admin"])
|
||||
_send_push(
|
||||
recipients,
|
||||
f"PPE COMPLIANCE BREACH: Worker {doc.worker} submitted a spray log for Zone {doc.zone} "
|
||||
f"without full PPE confirmation. Immediate review required.",
|
||||
priority="CRITICAL",
|
||||
)
|
||||
|
||||
|
||||
def release_expired_reentry_locks():
|
||||
"""Scheduled every 15 minutes — clear restricted_until once REI has expired."""
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabGreenhouse Zone`
|
||||
SET restricted_until = NULL
|
||||
WHERE restricted_until IS NOT NULL AND restricted_until <= %s
|
||||
""",
|
||||
(now_datetime(),),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user