commit eea66ad8335ef72be732a39d2755d4caea6653ec Author: Joseph Njuki Date: Fri Jul 31 06:36:30 2026 +0300 Initial commit: full backend - 13 activity doctypes, JWT auth, generic activity API, QR badge generation, workspace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cf5d26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +.DS_Store +node_modules/ +*.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7f15d92 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include MANIFEST.in +recursive-include agri_seedling *.json *.md *.txt *.html *.css *.js *.py +recursive-exclude agri_seedling *.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/__init__.py b/agri_seedling/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/agri_seedling/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/agri_seedling/agri_seedling/__init__.py b/agri_seedling/agri_seedling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/api/activities.py b/agri_seedling/agri_seedling/api/activities.py new file mode 100644 index 0000000..ffe654f --- /dev/null +++ b/agri_seedling/agri_seedling/api/activities.py @@ -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"]), + } diff --git a/agri_seedling/agri_seedling/api/mobile_auth.py b/agri_seedling/agri_seedling/api/mobile_auth.py new file mode 100644 index 0000000..e3df841 --- /dev/null +++ b/agri_seedling/agri_seedling/api/mobile_auth.py @@ -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} diff --git a/agri_seedling/agri_seedling/api/worker_qr.py b/agri_seedling/agri_seedling/api/worker_qr.py new file mode 100644 index 0000000..c11da56 --- /dev/null +++ b/agri_seedling/agri_seedling/api/worker_qr.py @@ -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, + } diff --git a/agri_seedling/agri_seedling/doctype/__init__.py b/agri_seedling/agri_seedling/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/cover_log/__init__.py b/agri_seedling/agri_seedling/doctype/cover_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/cover_log/cover_log.js b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.js new file mode 100644 index 0000000..38e3329 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json new file mode 100644 index 0000000..4961032 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/cover_log/cover_log.py b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.py new file mode 100644 index 0000000..f920986 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/cover_log/test_cover_log.py b/agri_seedling/agri_seedling/doctype/cover_log/test_cover_log.py new file mode 100644 index 0000000..63d33b0 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/cover_log/test_cover_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/__init__.py b/agri_seedling/agri_seedling/doctype/customer_interaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json new file mode 100644 index 0000000..9df9c5e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.py b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.py new file mode 100644 index 0000000..5bafdcf --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class CustomerInteraction(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/customer_profile/__init__.py b/agri_seedling/agri_seedling/doctype/customer_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.js b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.js new file mode 100644 index 0000000..de3967a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.json b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.json new file mode 100644 index 0000000..20bd68a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.py b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.py new file mode 100644 index 0000000..ad278a0 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_profile/customer_profile.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/customer_profile/test_customer_profile.py b/agri_seedling/agri_seedling/doctype/customer_profile/test_customer_profile.py new file mode 100644 index 0000000..cc7a406 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_profile/test_customer_profile.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/__init__.py b/agri_seedling/agri_seedling/doctype/dispatch_record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json new file mode 100644 index 0000000..7459d8b --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.py b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.py new file mode 100644 index 0000000..72915ba --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class DispatchRecord(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/__init__.py b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.js b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.js new file mode 100644 index 0000000..8b6d0c4 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.json b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.json new file mode 100644 index 0000000..575db20 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.py b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.py new file mode 100644 index 0000000..4a0d2b6 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/greenhouse_watering_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/test_greenhouse_watering_log.py b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/test_greenhouse_watering_log.py new file mode 100644 index 0000000..89f1b3b --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_watering_log/test_greenhouse_watering_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_zone/__init__.py b/agri_seedling/agri_seedling/doctype/greenhouse_zone/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.json b/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.json new file mode 100644 index 0000000..4068ff7 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.py b/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.py new file mode 100644 index 0000000..d17d449 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/greenhouse_zone/greenhouse_zone.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class GreenhouseZone(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/media_mix_batch/__init__.py b/agri_seedling/agri_seedling/doctype/media_mix_batch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.js b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.js new file mode 100644 index 0000000..90639c2 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.json b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.json new file mode 100644 index 0000000..a7c46b7 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.py b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.py new file mode 100644 index 0000000..322ecfe --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/media_mix_batch/media_mix_batch.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/media_mix_batch/test_media_mix_batch.py b/agri_seedling/agri_seedling/doctype/media_mix_batch/test_media_mix_batch.py new file mode 100644 index 0000000..a2c59f3 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/media_mix_batch/test_media_mix_batch.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/order_patch/__init__.py b/agri_seedling/agri_seedling/doctype/order_patch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/order_patch/order_patch.js b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.js new file mode 100644 index 0000000..c77b930 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json new file mode 100644 index 0000000..b07c3be --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/order_patch/order_patch.py b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.py new file mode 100644 index 0000000..1a8c403 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/order_patch/test_order_patch.py b/agri_seedling/agri_seedling/doctype/order_patch/test_order_patch.py new file mode 100644 index 0000000..7632ba2 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/order_patch/test_order_patch.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/planting_record/__init__.py b/agri_seedling/agri_seedling/doctype/planting_record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json new file mode 100644 index 0000000..bed8928 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/planting_record/planting_record.py b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.py new file mode 100644 index 0000000..1006d06 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class PlantingRecord(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/__init__.py b/agri_seedling/agri_seedling/doctype/resource_inventory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json new file mode 100644 index 0000000..e58597e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.py b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.py new file mode 100644 index 0000000..ae8032d --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class ResourceInventory(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/__init__.py b/agri_seedling/agri_seedling/doctype/scouting_report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json new file mode 100644 index 0000000..54828cd --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py new file mode 100644 index 0000000..ce17227 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py @@ -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) diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/__init__.py b/agri_seedling/agri_seedling/doctype/seed_variety/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.js b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.js new file mode 100644 index 0000000..6620efd --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json new file mode 100644 index 0000000..08b329e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.py b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.py new file mode 100644 index 0000000..3b985fd --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/test_seed_variety.py b/agri_seedling/agri_seedling/doctype/seed_variety/test_seed_variety.py new file mode 100644 index 0000000..50e1094 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seed_variety/test_seed_variety.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/seedling_watering_log/__init__.py b/agri_seedling/agri_seedling/doctype/seedling_watering_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.js b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.js new file mode 100644 index 0000000..966e1da --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.json b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.json new file mode 100644 index 0000000..6bd0c17 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.py b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.py new file mode 100644 index 0000000..ae3a1ba --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seedling_watering_log/seedling_watering_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/seedling_watering_log/test_seedling_watering_log.py b/agri_seedling/agri_seedling/doctype/seedling_watering_log/test_seedling_watering_log.py new file mode 100644 index 0000000..034bf05 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/seedling_watering_log/test_seedling_watering_log.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/soaking_record/__init__.py b/agri_seedling/agri_seedling/doctype/soaking_record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.js b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.js new file mode 100644 index 0000000..5306ff8 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.json b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.json new file mode 100644 index 0000000..359c113 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.py b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.py new file mode 100644 index 0000000..2c0958a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/soaking_record/soaking_record.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/soaking_record/test_soaking_record.py b/agri_seedling/agri_seedling/doctype/soaking_record/test_soaking_record.py new file mode 100644 index 0000000..98a9621 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/soaking_record/test_soaking_record.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/spray_log_evening/__init__.py b/agri_seedling/agri_seedling/doctype/spray_log_evening/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.js b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.js new file mode 100644 index 0000000..db17551 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.json b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.json new file mode 100644 index 0000000..d7fe2b1 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.py b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.py new file mode 100644 index 0000000..f5b1555 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_evening/spray_log_evening.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/spray_log_evening/test_spray_log_evening.py b/agri_seedling/agri_seedling/doctype/spray_log_evening/test_spray_log_evening.py new file mode 100644 index 0000000..8c37efb --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_evening/test_spray_log_evening.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/spray_log_morning/__init__.py b/agri_seedling/agri_seedling/doctype/spray_log_morning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.json b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.json new file mode 100644 index 0000000..ee3f9db --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.py b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.py new file mode 100644 index 0000000..23296f4 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.py @@ -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.")) diff --git a/agri_seedling/agri_seedling/doctype/tray_recycling_record/__init__.py b/agri_seedling/agri_seedling/doctype/tray_recycling_record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/tray_recycling_record/test_tray_recycling_record.py b/agri_seedling/agri_seedling/doctype/tray_recycling_record/test_tray_recycling_record.py new file mode 100644 index 0000000..5a863d9 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_recycling_record/test_tray_recycling_record.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.js b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.js new file mode 100644 index 0000000..310679f --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.json b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.json new file mode 100644 index 0000000..a65bfe2 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.json @@ -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": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.py b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.py new file mode 100644 index 0000000..e07978e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_recycling_record/tray_recycling_record.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/tray_registry/__init__.py b/agri_seedling/agri_seedling/doctype/tray_registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.json b/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.json new file mode 100644 index 0000000..7908b96 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.py b/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.py new file mode 100644 index 0000000..e878786 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_registry/tray_registry.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class TrayRegistry(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/worker_activity_assignment/__init__.py b/agri_seedling/agri_seedling/doctype/worker_activity_assignment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.json b/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.json new file mode 100644 index 0000000..da5b7af --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.json @@ -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"} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.py b/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.py new file mode 100644 index 0000000..f977efe --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_activity_assignment/worker_activity_assignment.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class WorkerActivityAssignment(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/worker_profile/__init__.py b/agri_seedling/agri_seedling/doctype/worker_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/worker_profile/test_worker_profile.py b/agri_seedling/agri_seedling/doctype/worker_profile/test_worker_profile.py new file mode 100644 index 0000000..c776f47 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_profile/test_worker_profile.py @@ -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 diff --git a/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.js b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.js new file mode 100644 index 0000000..700671a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.js @@ -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) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.json b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.json new file mode 100644 index 0000000..b927146 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.json @@ -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 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.py b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.py new file mode 100644 index 0000000..db3f561 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class WorkerProfile(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/worker_zone_assignment/__init__.py b/agri_seedling/agri_seedling/doctype/worker_zone_assignment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.json b/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.json new file mode 100644 index 0000000..c6cfcb9 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.json @@ -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} + ] +} diff --git a/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.py b/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.py new file mode 100644 index 0000000..dc6ec40 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/worker_zone_assignment/worker_zone_assignment.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class WorkerZoneAssignment(Document): + pass diff --git a/agri_seedling/agri_seedling/report/__init__.py b/agri_seedling/agri_seedling/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/report/daily_resource_consumption/__init__.py b/agri_seedling/agri_seedling/report/daily_resource_consumption/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.py b/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.py new file mode 100644 index 0000000..5c3b787 --- /dev/null +++ b/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.py @@ -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"
{rows}
", + ) + + +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 diff --git a/agri_seedling/agri_seedling/utils/__init__.py b/agri_seedling/agri_seedling/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/utils/auth.py b/agri_seedling/agri_seedling/utils/auth.py new file mode 100644 index 0000000..6fc988b --- /dev/null +++ b/agri_seedling/agri_seedling/utils/auth.py @@ -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, + ) diff --git a/agri_seedling/agri_seedling/utils/compliance.py b/agri_seedling/agri_seedling/utils/compliance.py new file mode 100644 index 0000000..512f520 --- /dev/null +++ b/agri_seedling/agri_seedling/utils/compliance.py @@ -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(),), + ) diff --git a/agri_seedling/agri_seedling/utils/jigs.py b/agri_seedling/agri_seedling/utils/jigs.py new file mode 100644 index 0000000..dcc466b --- /dev/null +++ b/agri_seedling/agri_seedling/utils/jigs.py @@ -0,0 +1,11 @@ +import frappe + + +def check_jig_maintenance_due(): + """Daily scheduled job — jigs at/above 200 usage-hours trigger a LOW + priority reminder to Stock Manager (Sec 5.12).""" + if not frappe.db.table_exists("Jig"): + return + due = frappe.get_all("Jig", filters={"usage_hours": [">=", 200], "maintenance_flagged": 0}, pluck="name") + for jig in due: + frappe.db.set_value("Jig", jig, "maintenance_flagged", 1) diff --git a/agri_seedling/agri_seedling/utils/notifications.py b/agri_seedling/agri_seedling/utils/notifications.py new file mode 100644 index 0000000..db18fc3 --- /dev/null +++ b/agri_seedling/agri_seedling/utils/notifications.py @@ -0,0 +1,52 @@ +import frappe + + +def notify_scouting_alert(doc, method=None): + """Severity >= 3 -> Critical push to Admin + Stock Manager (Sec 6.2).""" + if not doc.severity or doc.severity < 3: + return + recipients = _users_with_role(["Admin", "Stock Manager"]) + message = ( + f"PEST ALERT: {doc.pest_disease_type or 'Issue'} detected in Zone {doc.zone} " + f"— Severity {doc.severity}. Worker: {doc.worker}. Immediate action required." + ) + _send_push(recipients, message, priority="CRITICAL") + + +def notify_dispatch_complete(doc, method=None): + recipients = _users_with_role(["Admin", "Accountant"]) + message = f"Order {doc.order} dispatched to {doc.customer} on {doc.dispatch_datetime}." + _send_email(recipients, subject=f"Order {doc.order} Dispatched", message=message) + + +def notify_order_patched(doc, method=None): + recipients = _users_with_role(["Stock Manager"]) + message = f"Order {doc.name} has been patched and scheduled for production." + _send_push(recipients, message, priority="HIGH") + + +def _users_with_role(roles): + users = set() + for role in roles: + users.update(frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"}, pluck="parent")) + return list(users) + + +def _send_push(recipients, message, priority="NORMAL"): + # TODO: wire to Firebase Cloud Messaging. Placeholder writes to the + # Notification Log so behavior is visible during development. + for user in recipients: + frappe.get_doc( + { + "doctype": "Notification Log", + "for_user": user, + "type": "Alert", + "subject": message, + } + ).insert(ignore_permissions=True) + + +def _send_email(recipients, subject, message): + if not recipients: + return + frappe.sendmail(recipients=recipients, subject=subject, message=message) diff --git a/agri_seedling/agri_seedling/utils/permissions.py b/agri_seedling/agri_seedling/utils/permissions.py new file mode 100644 index 0000000..3aa7efb --- /dev/null +++ b/agri_seedling/agri_seedling/utils/permissions.py @@ -0,0 +1,17 @@ +import frappe + + +def field_worker_match_condition(user): + """Returns a SQL condition string. Field Workers only see their own + submissions; all other roles are unrestricted (Sec 9.2).""" + user = user or frappe.session.user + if "Field Worker" not in frappe.get_roles(user) or user in ("Administrator",): + return "" + return f"`owner` = {frappe.db.escape(user)}" + + +def field_worker_has_permission(doc, user=None, permission_type=None): + user = user or frappe.session.user + if "Field Worker" not in frappe.get_roles(user): + return True + return doc.owner == user diff --git a/agri_seedling/agri_seedling/utils/stock.py b/agri_seedling/agri_seedling/utils/stock.py new file mode 100644 index 0000000..8d19b7c --- /dev/null +++ b/agri_seedling/agri_seedling/utils/stock.py @@ -0,0 +1,69 @@ +import frappe +from frappe import _ + + +def deduct_spray_resources(doc, method=None): + if doc.product: + _deduct(doc.product, doc.solution_volume_sprayed or 0) + + +def reverse_spray_resources(doc, method=None): + if doc.product: + _deduct(doc.product, -(doc.solution_volume_sprayed or 0)) + + +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 seed_resource: + _deduct(seed_resource, doc.seed_quantity or 0) + + +def deduct_dispatch_resources(doc, method=None): + mapping = { + "Boxes": doc.boxes_used or 0, + "Tapes": doc.tape_rolls_used or 0, + "Rope": doc.rope_meters_used or 0, + "Fuel": doc.fuel_liters or 0, + } + for category, qty in mapping.items(): + if qty: + resource = frappe.db.get_value("Resource Inventory", {"category": category}, "name") + if resource: + _deduct(resource, qty) + + +def _deduct(resource_name, qty): + 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) + check_low_stock_threshold_by_name(resource_name) + + +def check_low_stock_threshold(doc, method=None): + check_low_stock_threshold_by_name(doc.name) + + +def check_low_stock_threshold_by_name(resource_name): + stock, threshold, priority = frappe.db.get_value( + "Resource Inventory", resource_name, ["current_stock", "reorder_threshold", "alert_priority"] + ) + if threshold is not None and stock is not None and stock < threshold: + from agri_seedling.agri_seedling.utils.notifications import _send_push, _users_with_role + + recipients = _users_with_role(["Stock Manager", "Admin"]) + _send_push( + recipients, + _("Low Stock: {0} has fallen below the reorder threshold. Current stock: {1}.").format( + resource_name, stock + ), + priority=priority or "HIGH", + ) + + +def run_low_stock_scan(): + """Daily scheduled job — sweep all resources for threshold breaches.""" + for r in frappe.get_all("Resource Inventory", pluck="name"): + check_low_stock_threshold_by_name(r) diff --git a/agri_seedling/agri_seedling/utils/sync.py b/agri_seedling/agri_seedling/utils/sync.py new file mode 100644 index 0000000..0041db2 --- /dev/null +++ b/agri_seedling/agri_seedling/utils/sync.py @@ -0,0 +1,23 @@ +import frappe +from frappe.utils import add_days, now_datetime + + +def warn_stale_offline_records(): + """Daily job — flag mobile sync records pending > 7 days (Sec 8.2).""" + if not frappe.db.table_exists("Mobile Sync Queue"): + return + cutoff = add_days(now_datetime(), -7) + stale = frappe.get_all( + "Mobile Sync Queue", + filters={"status": "Pending Sync", "creation": ["<", cutoff]}, + pluck="owner", + ) + if stale: + from agri_seedling.agri_seedling.utils.notifications import _send_push, _users_with_role + + recipients = _users_with_role(["Admin"]) + _send_push( + recipients, + f"{len(stale)} offline record(s) have been pending sync for over 7 days — possible worker connectivity issue.", + priority="LOW", + ) diff --git a/agri_seedling/agri_seedling/utils/tasks.py b/agri_seedling/agri_seedling/utils/tasks.py new file mode 100644 index 0000000..66ffb49 --- /dev/null +++ b/agri_seedling/agri_seedling/utils/tasks.py @@ -0,0 +1,7 @@ +import frappe + + +def send_due_activity_reminders(): + """Hourly job — placeholder for spray/germination/uncovering reminders + driven by each activity's scheduled task doctype (Sec 6.2).""" + pass diff --git a/agri_seedling/agri_seedling/utils/tray_registry.py b/agri_seedling/agri_seedling/utils/tray_registry.py new file mode 100644 index 0000000..f96a215 --- /dev/null +++ b/agri_seedling/agri_seedling/utils/tray_registry.py @@ -0,0 +1,27 @@ +import frappe +from frappe.utils import now_datetime + + +def create_tray_entries(doc, method=None): + """On Planting Record submit — register each tray as Planted.""" + for i in range(1, (doc.tray_count or 0) + 1): + tray_id = f"{doc.tray_batch_id}-{i:03d}" + if not frappe.db.exists("Tray Registry", tray_id): + frappe.get_doc( + { + "doctype": "Tray Registry", + "tray_id": tray_id, + "status": "Planted", + "planting_record": doc.name, + "zone": doc.zone, + "growth_stage": "Germination", + "last_updated": now_datetime(), + } + ).insert(ignore_permissions=True) + + +def return_trays_to_stock(doc, method=None): + """On Tray Recycling Record submit — mark reusable trays Available.""" + # Field-level detail depends on the recycling child-table structure; + # implement condition-by-condition status updates here. + pass diff --git a/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json b/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json new file mode 100644 index 0000000..a086790 --- /dev/null +++ b/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json @@ -0,0 +1,129 @@ +{ + "charts": [], + "content": "[{\"id\": \"header1\", \"type\": \"header\", \"data\": {\"text\": \"Field Worker Activities\", \"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}}]", + "creation": "2026-07-31 01:37:26.854909", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "hide_custom": 0, + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Agri Seedling", + "links": [], + "modified": "2026-07-31 01:55:10.299652", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Agri Seedling", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 0.0, + "shortcuts": [ + { + "doc_view": "List", + "label": "Scouting Report", + "link_to": "Scouting Report", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Spray Log Morning", + "link_to": "Spray Log Morning", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Planting Record", + "link_to": "Planting Record", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Dispatch Record", + "link_to": "Dispatch Record", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Tray Registry", + "link_to": "Tray Registry", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Customer Interaction", + "link_to": "Customer Interaction", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Resource Inventory", + "link_to": "Resource Inventory", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Greenhouse Zone", + "link_to": "Greenhouse Zone", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Order Patch", + "link_to": "Order Patch", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Worker Profile", + "link_to": "Worker Profile", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Media Mix Batch", + "link_to": "Media Mix Batch", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Cover Log", + "link_to": "Cover Log", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Greenhouse Watering Log", + "link_to": "Greenhouse Watering Log", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Seedling Watering Log", + "link_to": "Seedling Watering Log", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Spray Log Evening", + "link_to": "Spray Log Evening", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Soaking Record", + "link_to": "Soaking Record", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Tray Recycling Record", + "link_to": "Tray Recycling Record", + "type": "DocType" + } + ], + "title": "Agri Seedling" +} \ No newline at end of file diff --git a/agri_seedling/config/__init__.py b/agri_seedling/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/config/desktop.py b/agri_seedling/config/desktop.py new file mode 100644 index 0000000..e4b437b --- /dev/null +++ b/agri_seedling/config/desktop.py @@ -0,0 +1,15 @@ +from frappe import _ + + +def get_data(): + return [ + { + "module_name": "Agri Seedling", + "category": "Modules", + "label": _("Agri Seedling"), + "color": "green", + "icon": "octicon octicon-globe", + "type": "module", + "description": "Farm management: field activities, resources, dispatch, and reporting.", + } + ] diff --git a/agri_seedling/hooks.py b/agri_seedling/hooks.py new file mode 100644 index 0000000..4c974e7 --- /dev/null +++ b/agri_seedling/hooks.py @@ -0,0 +1,141 @@ +from . import __version__ as app_version + +app_name = "agri_seedling" +app_title = "Agri Seedling" +app_publisher = "Agri Seedling Operations Team" +app_description = "Farm Management System — field activities, resource tracking, dispatch and reporting" +app_email = "sysadmin@agriseedling.com" +app_license = "Proprietary" + +# Apps +# ------------------ +# required_apps = ["erpnext"] + +# Includes in +# ------------------ +# app_include_css = "/assets/agri_seedling/css/agri_seedling.css" +# app_include_js = "/assets/agri_seedling/js/agri_seedling.js" + +# Home Pages +# ---------- +# role_home_page = {"Field Worker": "agri-seedling-mobile"} + +# Fixtures — exported with `bench export-fixtures`, keeps Roles/Custom Fields +# in version control across environments +# --------------------------------------------------------------------------- +fixtures = [ + {"dt": "Role", "filters": [["role_name", "in", [ + "Admin", + "Stock Manager", + "Inventory Manager", + "Accountant", + "Field Worker", + ]]]}, + "Notification", + {"dt": "Print Format", "filters": [["module", "=", "Agri Seedling"]]}, +] + +# Document Events +# --------------- +# Hook on document methods and events. Mirrors Chapter 4/6/9 of the manual: +# stock deduction, notifications, and immutability of submitted spray logs. +doc_events = { + "Scouting Report": { + "on_submit": "agri_seedling.agri_seedling.utils.notifications.notify_scouting_alert", + }, + "Spray Log Morning": { + "validate": "agri_seedling.agri_seedling.utils.compliance.enforce_ppe_checklist", + "on_submit": [ + "agri_seedling.agri_seedling.utils.stock.deduct_spray_resources", + "agri_seedling.agri_seedling.utils.compliance.start_reentry_interval_lock", + ], + "on_cancel": "agri_seedling.agri_seedling.utils.stock.reverse_spray_resources", + }, + "Spray Log Evening": { + "validate": "agri_seedling.agri_seedling.utils.compliance.enforce_ppe_checklist", + "on_submit": [ + "agri_seedling.agri_seedling.utils.stock.deduct_spray_resources", + "agri_seedling.agri_seedling.utils.compliance.start_reentry_interval_lock", + ], + "on_cancel": "agri_seedling.agri_seedling.utils.stock.reverse_spray_resources", + }, + "Planting Record": { + "on_submit": [ + "agri_seedling.agri_seedling.utils.stock.deduct_seed_stock", + "agri_seedling.agri_seedling.utils.tray_registry.create_tray_entries", + ], + }, + "Dispatch Record": { + "on_submit": [ + "agri_seedling.agri_seedling.utils.stock.deduct_dispatch_resources", + "agri_seedling.agri_seedling.utils.notifications.notify_dispatch_complete", + ], + }, + "Order Patch": { + "on_submit": "agri_seedling.agri_seedling.utils.notifications.notify_order_patched", + }, + "Tray Recycling Record": { + "on_submit": "agri_seedling.agri_seedling.utils.tray_registry.return_trays_to_stock", + }, + "Resource Inventory": { + "on_update": "agri_seedling.agri_seedling.utils.stock.check_low_stock_threshold", + }, +} + +# Scheduled Tasks +# --------------- +# Maps to Chapter 6 (notification triggers) and Chapter 5 (reorder alerts) +scheduler_events = { + "cron": { + # Daily 18:00 — resource consumption report to Stock Manager + Admin + "0 18 * * *": [ + "agri_seedling.agri_seedling.report.daily_resource_consumption.daily_resource_consumption.send_daily_report" + ], + # Every 15 min — release zones whose Re-Entry Interval has expired + "*/15 * * * *": [ + "agri_seedling.agri_seedling.utils.compliance.release_expired_reentry_locks" + ], + }, + "daily": [ + "agri_seedling.agri_seedling.utils.stock.run_low_stock_scan", + "agri_seedling.agri_seedling.utils.jigs.check_jig_maintenance_due", + "agri_seedling.agri_seedling.utils.sync.warn_stale_offline_records", + ], + "hourly": [ + "agri_seedling.agri_seedling.utils.tasks.send_due_activity_reminders", + ], +} + +# Permission Query Conditions +# --------------------------- +# Row-level isolation for Field Workers (Sec 9.2): can only read own submissions. +permission_query_conditions = { + "Scouting Report": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Spray Log Morning": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Spray Log Evening": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Planting Record": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Soaking Record": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Greenhouse Watering Log": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Seedling Watering Log": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Cover Log": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", + "Tray Recycling Record": "agri_seedling.agri_seedling.utils.permissions.field_worker_match_condition", +} + +has_permission = { + "Scouting Report": "agri_seedling.agri_seedling.utils.permissions.field_worker_has_permission", +} + +# Authentication / Session +# ------------------------ +# Field Worker (Guest User) sessions expire at midnight — see Sec 9.1 +# Implemented via a login hook that sets a short-lived session for the +# "Field Worker" role; see agri_seedling/utils/auth.py +on_session_creation = "agri_seedling.agri_seedling.utils.auth.set_field_worker_session_expiry" + +# Website Route Rules (kept minimal — mobile app is the field-worker client) +# --------------------------------------------------------------------------- +website_route_rules = [] + +# override_whitelisted_methods = { +# "agri_seedling.api.mobile_sync.push_activity": "agri_seedling.agri_seedling.api.mobile_sync.push_activity", +# } diff --git a/agri_seedling/modules.txt b/agri_seedling/modules.txt new file mode 100644 index 0000000..ef66ef0 --- /dev/null +++ b/agri_seedling/modules.txt @@ -0,0 +1 @@ +Agri Seedling diff --git a/agri_seedling/patches.txt b/agri_seedling/patches.txt new file mode 100644 index 0000000..08a1870 --- /dev/null +++ b/agri_seedling/patches.txt @@ -0,0 +1,2 @@ +# List patch modules here, e.g. +# agri_seedling.patches.v0_0.create_default_greenhouse_zones diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..8ca3b23 --- /dev/null +++ b/license.txt @@ -0,0 +1 @@ +Proprietary - Internal Use Only - Agri Seedling diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f47ffaf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +frappe diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..610a2e1 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup, find_packages + +with open("requirements.txt") as f: + install_requires = f.read().strip().split("\n") + +setup( + name="agri_seedling", + version="0.0.1", + description="Agri Seedling Farm Management System", + author="Agri Seedling Operations Team", + author_email="sysadmin@agriseedling.com", + packages=find_packages(), + zip_safe=False, + include_package_data=True, + install_requires=install_requires, +)