From 3a7e4e2dade6906ed0c6fa919e4ebf7aa21d910d Mon Sep 17 00:00:00 2001 From: KabundiNjuki Date: Thu, 27 Aug 2026 10:59:50 +0300 Subject: [PATCH] Backup current agri_seedling application --- .gitignore | 5 + agri_seedling/agri_seedling/api/__init__.py | 0 agri_seedling/agri_seedling/api/activities.py | 208 ++++++++----- .../agri_seedling/api/mobile_auth.py | 128 ++++---- agri_seedling/agri_seedling/auth_hooks.py | 55 ++++ .../doctype/chemical/__init__.py | 0 .../doctype/chemical/chemical.js | 8 + .../doctype/chemical/chemical.json | 114 +++++++ .../doctype/chemical/chemical.py | 9 + .../doctype/chemical/test_chemical.py | 9 + .../doctype/cover_log/cover_log.json | 7 + .../agri_seedling/doctype/crop/__init__.py | 0 .../agri_seedling/doctype/crop/crop.js | 8 + .../agri_seedling/doctype/crop/crop.json | 95 ++++++ .../agri_seedling/doctype/crop/crop.py | 9 + .../agri_seedling/doctype/crop/test_crop.py | 9 + .../doctype/customer/__init__.py | 0 .../doctype/customer/customer.js | 8 + .../doctype/customer/customer.json | 111 +++++++ .../doctype/customer/customer.py | 9 + .../doctype/customer/test_customer.py | 9 + .../customer_interaction.js | 8 + .../customer_interaction.json | 170 +++++++++-- .../test_customer_interaction.py | 9 + .../dispatch_record/dispatch_record.js | 8 + .../dispatch_record/dispatch_record.json | 221 ++++++++++++-- .../dispatch_record/test_dispatch_record.py | 9 + .../media_mix_batch/media_mix_batch.json | 14 +- .../doctype/order_patch/order_patch.json | 14 + .../agri_seedling/doctype/pest/__init__.py | 0 .../agri_seedling/doctype/pest/pest.js | 8 + .../agri_seedling/doctype/pest/pest.json | 87 ++++++ .../agri_seedling/doctype/pest/pest.py | 9 + .../agri_seedling/doctype/pest/test_pest.py | 9 + .../planting_record/planting_record.js | 8 + .../planting_record/planting_record.json | 196 ++++++++++-- .../planting_record/test_planting_record.py | 9 + .../resource_consumption_log/__init__.py | 0 .../resource_consumption_log.json | 117 ++++++++ .../resource_consumption_log.py | 6 + .../resource_inventory/resource_inventory.js | 8 + .../resource_inventory.json | 192 +++++++++--- .../test_resource_inventory.py | 9 + .../scouting_report/scouting_report.js | 8 + .../scouting_report/scouting_report.json | 216 +++++++++++--- .../scouting_report/scouting_report.py | 5 + .../scouting_report/test_scouting_report.py | 9 + .../doctype/seed_variety/seed_variety.json | 14 +- .../spray_log_evening/spray_log_evening.json | 24 +- .../spray_log_morning/spray_log_morning.js | 8 + .../spray_log_morning/spray_log_morning.json | 281 +++++++++++++++--- .../test_spray_log_morning.py | 9 + .../doctype/tray_type/__init__.py | 0 .../doctype/tray_type/test_tray_type.py | 9 + .../doctype/tray_type/tray_type.js | 8 + .../doctype/tray_type/tray_type.json | 85 ++++++ .../doctype/tray_type/tray_type.py | 9 + .../doctype/worker_profile/worker_profile.js | 26 +- .../worker_id_badge/worker_id_badge.json | 23 ++ .../daily_resource_consumption.json | 23 ++ .../daily_resource_usage_summary/__init__.py | 0 .../daily_resource_usage_summary.js | 42 +++ .../daily_resource_usage_summary.json | 23 ++ .../daily_resource_usage_summary.py | 61 ++++ agri_seedling/agri_seedling/utils/stock.py | 65 +++- .../agri_seedling/agri_seedling.json | 54 +++- agri_seedling/hooks.py | 9 + agri_seedling/requirements.txt | 1 + requirements.txt | 1 + 69 files changed, 2568 insertions(+), 357 deletions(-) create mode 100644 agri_seedling/agri_seedling/api/__init__.py create mode 100644 agri_seedling/agri_seedling/auth_hooks.py create mode 100644 agri_seedling/agri_seedling/doctype/chemical/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/chemical/chemical.js create mode 100644 agri_seedling/agri_seedling/doctype/chemical/chemical.json create mode 100644 agri_seedling/agri_seedling/doctype/chemical/chemical.py create mode 100644 agri_seedling/agri_seedling/doctype/chemical/test_chemical.py create mode 100644 agri_seedling/agri_seedling/doctype/crop/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/crop/crop.js create mode 100644 agri_seedling/agri_seedling/doctype/crop/crop.json create mode 100644 agri_seedling/agri_seedling/doctype/crop/crop.py create mode 100644 agri_seedling/agri_seedling/doctype/crop/test_crop.py create mode 100644 agri_seedling/agri_seedling/doctype/customer/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/customer/customer.js create mode 100644 agri_seedling/agri_seedling/doctype/customer/customer.json create mode 100644 agri_seedling/agri_seedling/doctype/customer/customer.py create mode 100644 agri_seedling/agri_seedling/doctype/customer/test_customer.py create mode 100644 agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.js create mode 100644 agri_seedling/agri_seedling/doctype/customer_interaction/test_customer_interaction.py create mode 100644 agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.js create mode 100644 agri_seedling/agri_seedling/doctype/dispatch_record/test_dispatch_record.py create mode 100644 agri_seedling/agri_seedling/doctype/pest/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/pest/pest.js create mode 100644 agri_seedling/agri_seedling/doctype/pest/pest.json create mode 100644 agri_seedling/agri_seedling/doctype/pest/pest.py create mode 100644 agri_seedling/agri_seedling/doctype/pest/test_pest.py create mode 100644 agri_seedling/agri_seedling/doctype/planting_record/planting_record.js create mode 100644 agri_seedling/agri_seedling/doctype/planting_record/test_planting_record.py create mode 100644 agri_seedling/agri_seedling/doctype/resource_consumption_log/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.json create mode 100644 agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.py create mode 100644 agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.js create mode 100644 agri_seedling/agri_seedling/doctype/resource_inventory/test_resource_inventory.py create mode 100644 agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.js create mode 100644 agri_seedling/agri_seedling/doctype/scouting_report/test_scouting_report.py create mode 100644 agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.js create mode 100644 agri_seedling/agri_seedling/doctype/spray_log_morning/test_spray_log_morning.py create mode 100644 agri_seedling/agri_seedling/doctype/tray_type/__init__.py create mode 100644 agri_seedling/agri_seedling/doctype/tray_type/test_tray_type.py create mode 100644 agri_seedling/agri_seedling/doctype/tray_type/tray_type.js create mode 100644 agri_seedling/agri_seedling/doctype/tray_type/tray_type.json create mode 100644 agri_seedling/agri_seedling/doctype/tray_type/tray_type.py create mode 100644 agri_seedling/agri_seedling/print_format/worker_id_badge/worker_id_badge.json create mode 100644 agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.json create mode 100644 agri_seedling/agri_seedling/report/daily_resource_usage_summary/__init__.py create mode 100644 agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.js create mode 100644 agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.json create mode 100644 agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.py create mode 100644 agri_seedling/requirements.txt diff --git a/.gitignore b/.gitignore index 7cf5d26..dfc4942 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ __pycache__/ .DS_Store node_modules/ *.egg-info/ +*.bak +*.backup +.env +*.sql +*.sql.gz diff --git a/agri_seedling/agri_seedling/api/__init__.py b/agri_seedling/agri_seedling/api/__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 index ffe654f..3a80893 100644 --- a/agri_seedling/agri_seedling/api/activities.py +++ b/agri_seedling/agri_seedling/api/activities.py @@ -1,82 +1,150 @@ import json - import frappe +from frappe import _ +from agri_seedling.agri_seedling.auth_hooks import get_current_worker -from agri_seedling.agri_seedling.api.mobile_auth import get_current_field_worker - -# doctype -> fieldname that should be auto-stamped with the logged-in worker -ACTIVITY_DOCTYPES = { - "Scouting Report": "worker", - "Order Patch": None, - "Dispatch Record": None, - "Media Mix Batch": "worker", - "Planting Record": "worker", - "Cover Log": "worker", - "Greenhouse Watering Log": "worker", - "Seedling Watering Log": "worker", - "Spray Log Morning": "worker", - "Spray Log Evening": "worker", - "Customer Interaction": None, - "Soaking Record": "worker", - "Tray Recycling Record": "worker", +# Only these may ever be created from the mobile app. +ALLOWED_DOCTYPES = { + "Scouting Report", "Spray Log Morning", "Spray Log Evening", + "Planting Record", "Soaking Record", "Tray Registry", + "Greenhouse Watering Log", "Seedling Watering Log", "Dispatch Record", + "Tray Recycling Record", "Media Mix Batch", "Cover Log", + "Customer Interaction", "Order Patch", "Resource Inventory", } +def _profile(): + # This calls your hook to ensure the token is valid + return frappe.get_doc("Worker Profile", get_current_worker()) -@frappe.whitelist(allow_guest=True) -def submit_activity(doctype, data): - """Generic activity submission endpoint used by the React Native app - for all 13 field activities. Requires a valid field-worker JWT.""" - worker = get_current_field_worker() +def _assigned_zones(profile): + return [r.zone for r in (profile.get("assigned_zones") or []) if r.get("zone")] - if doctype not in ACTIVITY_DOCTYPES: - frappe.throw(f"Unknown or disallowed activity doctype: {doctype}") - - if isinstance(data, str): - data = json.loads(data) - - data["doctype"] = doctype - - worker_field = ACTIVITY_DOCTYPES[doctype] - if worker_field: - data[worker_field] = worker["worker_id"] - - doc = frappe.get_doc(data) - doc.insert(ignore_permissions=True) - frappe.db.commit() - - return {"name": doc.name, "doctype": doctype, "status": "submitted"} - - -@frappe.whitelist(allow_guest=True) -def my_activities(doctype, limit=20): - """Returns the logged-in worker's own recent submissions for a - given activity doctype (field workers can only see their own, per - the row-level permission rule in Sec 9.2 of the manual).""" - worker = get_current_field_worker() - - if doctype not in ACTIVITY_DOCTYPES: - frappe.throw(f"Unknown or disallowed activity doctype: {doctype}") - - worker_field = ACTIVITY_DOCTYPES[doctype] - filters = {worker_field: worker["worker_id"]} if worker_field else {} - - return frappe.get_list( - doctype, - filters=filters, - fields=["name", "creation"], - order_by="creation desc", - limit_page_length=int(limit), - ) +ACTIVITY_LABEL_TO_DOCTYPE = { + "Scouting": "Scouting Report", "Order Patching": "Order Patch", + "Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch", + "Planting": "Planting Record", "Covering Seedlings": "Cover Log", + "Watering Greenhouse": "Greenhouse Watering Log", "Watering Seedlings": "Seedling Watering Log", + "Spraying Morning": "Spray Log Morning", "Spraying Evening": "Spray Log Evening", + "Customer Engagement": "Customer Interaction", "Soaking": "Soaking Record", + "Recycling Trays": "Tray Recycling Record", +} +def _assigned_activities(profile): + labels = [r.activity_type for r in (profile.get("assigned_activity_types") or []) if r.get("activity_type")] + return [ACTIVITY_LABEL_TO_DOCTYPE.get(label, label) for label in labels] @frappe.whitelist(allow_guest=True) def get_reference_data(): - """Dropdown/reference data the mobile app needs to populate forms - (zones, varieties, customers). Requires a valid field-worker JWT.""" - get_current_field_worker() + """Dropdown data, scoped to the caller. The app caches this for offline use.""" + profile = _profile() - return { - "greenhouse_zones": frappe.get_all("Greenhouse Zone", fields=["name"]), - "seed_varieties": frappe.get_all("Seed Variety", fields=["name"]), - "customers": frappe.get_all("Customer Profile", fields=["name"]), - } + return { + "zones": _assigned_zones(profile), + "activity_types": _assigned_activities(profile), + "crops": frappe.get_all("Crop", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Crop") else [], + "pests": frappe.get_all("Pest", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Pest") else [], + "chemicals": frappe.get_all("Chemical", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Chemical") else [], + "resources": frappe.get_all("Resource Inventory", pluck="resource_name", limit_page_length=0) + if frappe.db.table_exists("Resource Inventory") else [], + "seed_varieties": frappe.get_all("Seed Variety", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Seed Variety") else [], + "tray_types": frappe.get_all("Tray Type", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Tray Type") else [], + "media_batches": frappe.get_all("Media Mix Batch", pluck="name", order_by="creation desc", limit_page_length=20) + if frappe.db.table_exists("Media Mix Batch") else [], + "customers": frappe.get_all("Customer", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Customer") else [], + "customer_profiles": frappe.get_all("Customer Profile", pluck="name", limit_page_length=0) + if frappe.db.table_exists("Customer Profile") else [], + } + +@frappe.whitelist(allow_guest=True) +def submit_activity(doctype=None, data=None): + """Server-side enforcement of assignments.""" + if doctype is None or data is None: + # form_dict can arrive empty for JSON bodies in this app's request + # flow; fall back to reading the raw body directly. + try: + raw_body = frappe.local.request.get_data(as_text=True) + if raw_body: + body = json.loads(raw_body) + doctype = doctype or body.get("doctype") + data = data or body.get("data") + except Exception: + pass + + if not doctype or data is None: + frappe.throw(_("Missing doctype or data in submission."), frappe.ValidationError) + + worker_id = get_current_worker() + profile = frappe.get_doc("Worker Profile", worker_id) + + if doctype not in ALLOWED_DOCTYPES: + frappe.throw(_("{0} cannot be submitted from the mobile app.").format(doctype), frappe.PermissionError) + + if doctype not in _assigned_activities(profile): + frappe.throw(_("You are not assigned to submit {0}.").format(doctype), frappe.PermissionError) + + payload = json.loads(data) if isinstance(data, str) else (data or {}) + + doc = frappe.new_doc(doctype) + meta = frappe.get_meta(doctype) + for key, value in payload.items(): + if meta.has_field(key): + doc.set(key, value) + + if meta.has_field("worker"): + doc.set("worker", worker_id) + + # Belt-and-suspenders: don't rely solely on the DocType's schema-level + # "__user" default resolving correctly (that's exactly what broke + # before — a stray literal "user" string in the JSON instead of the + # magic "__user" token). Setting it explicitly here, the same way + # "worker" is set, guarantees it works regardless of DocType JSON state. + if meta.has_field("staff_member") and not doc.get("staff_member"): + doc.set("staff_member", frappe.session.user) + + doc.insert(ignore_permissions=True) + frappe.db.commit() + return {"name": doc.name, "doctype": doctype} + +@frappe.whitelist(allow_guest=True) +def my_activities(doctype, limit=20): + worker_id = get_current_worker() + return frappe.get_all( + doctype, + filters={"worker": worker_id}, + fields=["name", "creation"], + order_by="creation desc", + limit_page_length=int(limit), + ignore_permissions=True, + ) + +@frappe.whitelist(allow_guest=True) +def attach_file_to_record(doctype=None, name=None, fieldname=None, file_url=None): + if doctype is None or name is None or fieldname is None or file_url is None: + try: + raw_body = frappe.local.request.get_data(as_text=True) + if raw_body: + body = json.loads(raw_body) + doctype = doctype or body.get("doctype") + name = name or body.get("name") + fieldname = fieldname or body.get("fieldname") + file_url = file_url or body.get("file_url") + except Exception: + pass + + if not all([doctype, name, fieldname, file_url]): + frappe.throw(_("Missing doctype, name, fieldname or file_url."), frappe.ValidationError) + + worker_id = get_current_worker() + owner_worker = frappe.db.get_value(doctype, name, "worker") + if owner_worker != worker_id: + frappe.throw(_("You can only attach files to your own records."), frappe.PermissionError) + + frappe.db.set_value(doctype, name, fieldname, file_url) + frappe.db.commit() + return {"ok": True, "file_url": file_url} diff --git a/agri_seedling/agri_seedling/api/mobile_auth.py b/agri_seedling/agri_seedling/api/mobile_auth.py index e3df841..b5087e5 100644 --- a/agri_seedling/agri_seedling/api/mobile_auth.py +++ b/agri_seedling/agri_seedling/api/mobile_auth.py @@ -1,72 +1,86 @@ -import datetime - +import json +import secrets import frappe -import jwt -from frappe.utils import add_days, get_datetime, nowdate -from frappe.utils.password import get_decrypted_password +from datetime import datetime, timedelta +from frappe import _ +from frappe.utils import now_datetime +from frappe.utils.password import check_password, get_decrypted_password +TOKEN_TTL_HOURS = 24 +TOKEN_CACHE_PREFIX = "worker_token" -def get_jwt_secret(): - secret = frappe.conf.get("agri_seedling_jwt_secret") - if not secret: - frappe.throw("JWT secret not configured") - return secret +ACTIVITY_LABEL_TO_DOCTYPE = { + "Scouting": "Scouting Report", "Order Patching": "Order Patch", + "Order Dispatching": "Dispatch Record", "Mixing Media": "Media Mix Batch", + "Planting": "Planting Record", "Covering Seedlings": "Cover Log", + "Watering Greenhouse": "Greenhouse Watering Log", "Watering Seedlings": "Seedling Watering Log", + "Spraying Morning": "Spray Log Morning", "Spraying Evening": "Spray Log Evening", + "Customer Engagement": "Customer Interaction", "Soaking": "Soaking Record", + "Recycling Trays": "Tray Recycling Record", +} +def issue_token(worker_id): + token = secrets.token_urlsafe(32) + expires_at = now_datetime() + timedelta(hours=TOKEN_TTL_HOURS) + frappe.cache().set_value( + f"{TOKEN_CACHE_PREFIX}:{token}", + json.dumps({"worker_id": worker_id, "expires_at": expires_at.isoformat()}), + expires_in_sec=TOKEN_TTL_HOURS * 3600, + ) + return token, expires_at -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) def field_worker_login(worker_id, pin): - if not frappe.db.exists("Worker Profile", worker_id): - frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError) + worker_id = (worker_id or "").strip() + pin = (pin or "").strip() - worker = frappe.get_doc("Worker Profile", worker_id) + if not frappe.db.exists("Worker Profile", worker_id): + frappe.throw(f"Worker ID '{worker_id}' not found in system.", frappe.AuthenticationError) - if not worker.active: - frappe.throw("This worker account is deactivated", frappe.AuthenticationError) + # --- UNIVERSAL PIN CHECK --- + authenticated = False + + # Method 1: Standard Frappe Check + try: + check_password(worker_id, pin, "Worker Profile", "pin_hash") + authenticated = True + except: + pass - stored_pin = get_decrypted_password( - "Worker Profile", worker_id, fieldname="pin_hash", raise_exception=False - ) + # Method 2: Decryption Check (for Desk-saved PINs) + if not authenticated: + try: + stored = get_decrypted_password("Worker Profile", worker_id, fieldname="pin_hash") + if stored and str(stored) == str(pin): + authenticated = True + except: + pass - if not stored_pin or str(stored_pin) != str(pin): - frappe.throw("Invalid worker ID or PIN", frappe.AuthenticationError) + # Method 3: Direct DB Match (Fallback) + if not authenticated: + db_val = frappe.db.get_value("Worker Profile", worker_id, "pin_hash") + if db_val == pin: # Sometimes saved as plain text in dev + authenticated = True - midnight = get_datetime(add_days(nowdate(), 1)) + if not authenticated: + frappe.throw(f"The PIN you entered for {worker_id} is incorrect.", frappe.AuthenticationError) - payload = { - "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") + profile = frappe.get_doc("Worker Profile", worker_id) + if not profile.active: + frappe.throw("This worker account is deactivated.", frappe.AuthenticationError) - 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 [], - } + assigned_activities = [ + ACTIVITY_LABEL_TO_DOCTYPE.get(row.activity_type, row.activity_type) + for row in (profile.get("assigned_activity_types") or []) if row.get("activity_type") + ] + token, expires_at = issue_token(worker_id) + return { + "token": token, + "expires_at": expires_at.isoformat(), + "worker_id": profile.name, + "full_name": profile.full_name, + "assigned_zones": [row.zone for row in (profile.get("assigned_zones") or []) if row.get("zone")], + "assigned_activity_types": assigned_activities + } -def get_current_field_worker(): - token = frappe.get_request_header("X-Worker-Token") - if not token: - frappe.throw("Missing X-Worker-Token header", frappe.AuthenticationError) - - try: - payload = jwt.decode(token, get_jwt_secret(), algorithms=["HS256"]) - except jwt.ExpiredSignatureError: - frappe.throw("Session expired, please log in again", frappe.AuthenticationError) - except jwt.InvalidTokenError: - frappe.throw("Invalid session token", frappe.AuthenticationError) - - return payload - - -@frappe.whitelist(allow_guest=True) -def whoami(): - worker = get_current_field_worker() - return {"authenticated_as": worker} diff --git a/agri_seedling/agri_seedling/auth_hooks.py b/agri_seedling/agri_seedling/auth_hooks.py new file mode 100644 index 0000000..7c92398 --- /dev/null +++ b/agri_seedling/agri_seedling/auth_hooks.py @@ -0,0 +1,55 @@ +import frappe +import json +from frappe.utils import now_datetime, get_datetime + +TOKEN_CACHE_PREFIX = "worker_token" +FIELD_WORKER_PROXY_USER = "field-worker@agriseedling.local" + +def authenticate_worker_token(): + # Initialize the error as "Missing Token" + frappe.local.auth_error = "No X-Worker-Token found in request headers." + + try: + token = frappe.get_request_header("X-Worker-Token") + if not token: + return + + raw = frappe.cache().get_value(f"{TOKEN_CACHE_PREFIX}:{token}") + if not raw: + frappe.local.auth_error = "Session not found on server. You may have been logged out." + return + + payload = json.loads(raw) + worker_id = payload.get("worker_id") + expires_at_str = payload.get("expires_at") + + if get_datetime(expires_at_str) < now_datetime(): + frappe.local.auth_error = f"Session for {worker_id} expired at {expires_at_str}." + frappe.cache().delete_value(f"{TOKEN_CACHE_PREFIX}:{token}") + return + + if not frappe.db.exists("Worker Profile", worker_id): + frappe.local.auth_error = f"Worker Profile {worker_id} no longer exists." + return + + if not frappe.db.exists("User", FIELD_WORKER_PROXY_USER): + frappe.local.auth_error = f"Proxy User {FIELD_WORKER_PROXY_USER} is missing. Run the console fix." + return + + # Success + frappe.local.worker_id = worker_id + frappe.local.auth_error = None + frappe.logger().info(f"BEFORE set_user form_dict={dict(frappe.form_dict)}") + frappe.set_user(FIELD_WORKER_PROXY_USER) + frappe.logger().info(f"AFTER set_user form_dict={dict(frappe.form_dict)}") + + except Exception as e: + frappe.local.auth_error = f"Internal Auth Error: {str(e)}" + +def get_current_worker(): + worker_id = getattr(frappe.local, "worker_id", None) + if not worker_id: + # Get the specific error we just found + reason = getattr(frappe.local, "auth_error", "Unknown Session Error") + frappe.throw(reason, frappe.AuthenticationError) + return worker_id diff --git a/agri_seedling/agri_seedling/doctype/chemical/__init__.py b/agri_seedling/agri_seedling/doctype/chemical/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/chemical/chemical.js b/agri_seedling/agri_seedling/doctype/chemical/chemical.js new file mode 100644 index 0000000..49b192f --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/chemical/chemical.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("Chemical", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/chemical/chemical.json b/agri_seedling/agri_seedling/doctype/chemical/chemical.json new file mode 100644 index 0000000..73db35a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/chemical/chemical.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:chemical_name", + "creation": "2026-08-06 03:12:41.247624", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "chemical_name", + "category", + "active_ingredient", + "registration_number", + "linked_resource" + ], + "fields": [ + { + "fieldname": "chemical_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Chemical Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "category", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Category", + "options": "Fungicide\nPesticide\nFoliar" + }, + { + "fieldname": "active_ingredient", + "fieldtype": "Data", + "label": "Active Ingredient" + }, + { + "fieldname": "registration_number", + "fieldtype": "Data", + "label": "Registration Number" + }, + { + "fieldname": "linked_resource", + "fieldtype": "Link", + "label": "Linked Stock Item", + "options": "Resource Inventory", + "in_list_view": 1, + "description": "The Resource Inventory record this chemical's stock is tracked under. Required for spray logs to deduct stock." + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-06 03:12:41.247624", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Chemical", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Admin", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Inventory Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/agri_seedling/agri_seedling/doctype/chemical/chemical.py b/agri_seedling/agri_seedling/doctype/chemical/chemical.py new file mode 100644 index 0000000..2310f0e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/chemical/chemical.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 Chemical(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/chemical/test_chemical.py b/agri_seedling/agri_seedling/doctype/chemical/test_chemical.py new file mode 100644 index 0000000..3fa8ff7 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/chemical/test_chemical.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 TestChemical(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json index 4961032..fed8264 100644 --- a/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json +++ b/agri_seedling/agri_seedling/doctype/cover_log/cover_log.json @@ -92,6 +92,13 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "write": 1 } ], "row_format": "Dynamic", diff --git a/agri_seedling/agri_seedling/doctype/crop/__init__.py b/agri_seedling/agri_seedling/doctype/crop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/crop/crop.js b/agri_seedling/agri_seedling/doctype/crop/crop.js new file mode 100644 index 0000000..bb8a113 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/crop/crop.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("Crop", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/crop/crop.json b/agri_seedling/agri_seedling/doctype/crop/crop.json new file mode 100644 index 0000000..bec5237 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/crop/crop.json @@ -0,0 +1,95 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:crop_name", + "creation": "2026-08-06 03:10:23.618416", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "crop_name", + "category", + "notes", + "linked_resource" + ], + "fields": [ + { + "fieldname": "crop_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Crop Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "category", + "fieldtype": "Select", + "label": "Category", + "options": "Vegetable\nHerb\nFruit\nFlower\nOther" + }, + { + "fieldname": "notes", + "fieldtype": "Small Text", + "label": "Notes" + }, + { + "fieldname": "linked_resource", + "fieldtype": "Link", + "label": "Linked Seed Stock", + "options": "Resource Inventory", + "in_list_view": 1, + "description": "The Resource Inventory record this crop's seed stock is tracked under. Required for planting records to deduct stock." + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-06 03:10:23.618416", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Crop", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Admin", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/agri_seedling/agri_seedling/doctype/crop/crop.py b/agri_seedling/agri_seedling/doctype/crop/crop.py new file mode 100644 index 0000000..b578803 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/crop/crop.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 Crop(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/crop/test_crop.py b/agri_seedling/agri_seedling/doctype/crop/test_crop.py new file mode 100644 index 0000000..c362b8a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/crop/test_crop.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 TestCrop(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/customer/__init__.py b/agri_seedling/agri_seedling/doctype/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/customer/customer.js b/agri_seedling/agri_seedling/doctype/customer/customer.js new file mode 100644 index 0000000..18368f7 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer/customer.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", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/customer/customer.json b/agri_seedling/agri_seedling/doctype/customer/customer.json new file mode 100644 index 0000000..0cdfe1c --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer/customer.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:customer_name", + "creation": "2026-08-06 03:12:10.833021", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_name", + "phone", + "email", + "address", + "customer_type" + ], + "fields": [ + { + "fieldname": "customer_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Customer Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone" + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email" + }, + { + "fieldname": "address", + "fieldtype": "Small Text", + "label": "Address" + }, + { + "fieldname": "customer_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Customer Type", + "options": "Retail\nWholesale\nInstitutional" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-06 03:12:10.833021", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Customer", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Admin", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accountant", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/customer/customer.py b/agri_seedling/agri_seedling/doctype/customer/customer.py new file mode 100644 index 0000000..ac15624 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer/customer.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 Customer(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/customer/test_customer.py b/agri_seedling/agri_seedling/doctype/customer/test_customer.py new file mode 100644 index 0000000..45a6392 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer/test_customer.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 TestCustomer(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.js b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.js new file mode 100644 index 0000000..bd2741d --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.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 Interaction", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json index 9df9c5e..e45a570 100644 --- a/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json +++ b/agri_seedling/agri_seedling/doctype/customer_interaction/customer_interaction.json @@ -1,30 +1,148 @@ { - "doctype": "DocType", - "name": "Customer Interaction", - "module": "Agri Seedling", - "istable": 0, - "issingle": 0, - "is_submittable": 0, + "actions": [], "autoname": "naming_series:", - "naming_series": "CI-.YYYY.-.#####", - "track_changes": 1, - "fields": [ - {"fieldname": "interaction_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, - {"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "in_list_view": 1}, - {"fieldname": "interaction_type", "label": "Interaction Type", "fieldtype": "Select", "reqd": 1, "in_list_view": 1, - "options": "Phone Call\nFarm Visit\nEmail Inquiry\nComplaint\nOrder Inquiry"}, - {"fieldname": "seedling_variety", "label": "Seedling Variety / Order of Interest", "fieldtype": "Link", "options": "Seed Variety"}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "summary", "label": "Interaction Summary", "fieldtype": "Small Text", "reqd": 1}, - {"fieldname": "feedback", "label": "Customer Feedback", "fieldtype": "Select", "options": "Positive\nNeutral\nNegative"}, - {"fieldname": "follow_up_required", "label": "Follow-up Required", "fieldtype": "Check"}, - {"fieldname": "follow_up_task", "label": "Follow-up Task Description", "fieldtype": "Small Text", "depends_on": "follow_up_required"}, - {"fieldname": "follow_up_assignee", "label": "Assignee", "fieldtype": "Link", "options": "User", "depends_on": "follow_up_required"}, - {"fieldname": "next_contact_date", "label": "Next Contact Date", "fieldtype": "Date", "depends_on": "follow_up_required"}, - {"fieldname": "staff_member", "label": "Staff Member", "fieldtype": "Link", "options": "User", "default": "user"} + "creation": "2026-08-05 06:22:53.359748", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "interaction_datetime", + "customer", + "interaction_type", + "seedling_variety", + "column_break_1", + "summary", + "feedback", + "follow_up_required", + "follow_up_task", + "follow_up_assignee", + "next_contact_date", + "staff_member", + "worker", + "naming_series" ], + "fields": [ + { + "default": "Now", + "fieldname": "interaction_datetime", + "fieldtype": "Datetime", + "label": "Date & Time", + "reqd": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fieldname": "interaction_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Interaction Type", + "options": "Phone Call\nFarm Visit\nEmail Inquiry\nComplaint\nOrder Inquiry", + "reqd": 1 + }, + { + "fieldname": "seedling_variety", + "fieldtype": "Link", + "label": "Crop / Order of Interest", + "options": "Crop" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "summary", + "fieldtype": "Small Text", + "label": "Interaction Summary", + "reqd": 1 + }, + { + "fieldname": "feedback", + "fieldtype": "Select", + "label": "Customer Feedback", + "options": "Positive\nNeutral\nNegative" + }, + { + "default": "0", + "fieldname": "follow_up_required", + "fieldtype": "Check", + "label": "Follow-up Required" + }, + { + "depends_on": "follow_up_required", + "fieldname": "follow_up_task", + "fieldtype": "Small Text", + "label": "Follow-up Task Description" + }, + { + "depends_on": "follow_up_required", + "fieldname": "follow_up_assignee", + "fieldtype": "Link", + "label": "Assignee", + "options": "User" + }, + { + "depends_on": "follow_up_required", + "fieldname": "next_contact_date", + "fieldtype": "Date", + "label": "Next Contact Date" + }, + { + "default": "__user", + "fieldname": "staff_member", + "fieldtype": "Link", + "label": "Staff Member", + "options": "User" + }, + { + "fieldname": "worker", + "fieldtype": "Link", + "label": "Worker", + "options": "Worker Profile" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "CIN-.YYYY.-" + } + ], + "links": [], + "modified": "2026-08-06 03:22:46.763055", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Customer Interaction", + "owner": "Administrator", "permissions": [ - {"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "report": 1}, - {"role": "Stock Manager", "read": 1, "write": 1, "create": 1} - ] -} + { + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Admin", + "write": 1 + }, + { + "create": 1, + "read": 1, + "role": "Stock Manager", + "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/customer_interaction/test_customer_interaction.py b/agri_seedling/agri_seedling/doctype/customer_interaction/test_customer_interaction.py new file mode 100644 index 0000000..7278155 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/customer_interaction/test_customer_interaction.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 TestCustomerInteraction(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.js b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.js new file mode 100644 index 0000000..a069f78 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_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("Dispatch Record", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json index 7459d8b..15b4949 100644 --- a/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json +++ b/agri_seedling/agri_seedling/doctype/dispatch_record/dispatch_record.json @@ -1,37 +1,192 @@ { - "doctype": "DocType", - "name": "Dispatch Record", - "module": "Agri Seedling", - "istable": 0, - "issingle": 0, - "is_submittable": 1, + "actions": [], "autoname": "naming_series:", - "naming_series": "DSP-.YYYY.-.#####", - "track_changes": 1, - "fields": [ - {"fieldname": "order", "label": "Order", "fieldtype": "Link", "options": "Order Patch", "reqd": 1, "in_list_view": 1}, - {"fieldname": "customer", "label": "Customer", "fieldtype": "Link", "options": "Customer Profile", "reqd": 1, "fetch_from": "order.customer", "in_list_view": 1}, - {"fieldname": "delivery_address", "label": "Delivery Address", "fieldtype": "Small Text"}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "quantity_dispatched", "label": "Quantity Dispatched (trays)", "fieldtype": "Int", "reqd": 1}, - {"fieldname": "boxes_used", "label": "Boxes Used", "fieldtype": "Int"}, - {"fieldname": "box_batch_number", "label": "Box Batch Number", "fieldtype": "Data"}, - {"fieldname": "tape_rolls_used", "label": "Tape Rolls Used", "fieldtype": "Float"}, - {"fieldname": "rope_meters_used", "label": "Rope Meters Used", "fieldtype": "Float"}, - {"fieldname": "section_break_1", "label": "Transport", "fieldtype": "Section Break"}, - {"fieldname": "transport_mode", "label": "Transport Mode", "fieldtype": "Select", "options": "Company Vehicle\nThird-Party Courier"}, - {"fieldname": "driver_name", "label": "Driver Name", "fieldtype": "Data"}, - {"fieldname": "vehicle_transport_id", "label": "Vehicle / Transport ID", "fieldtype": "Data"}, - {"fieldname": "column_break_2", "fieldtype": "Column Break"}, - {"fieldname": "dispatch_datetime", "label": "Dispatch Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, - {"fieldname": "fuel_liters", "label": "Fuel Consumed (L)", "fieldtype": "Float"}, - {"fieldname": "quality_check_pass", "label": "Final QC Result", "fieldtype": "Select", "options": "Pass\nFail", "reqd": 1}, - {"fieldname": "quality_check_notes", "label": "QC Notes", "fieldtype": "Small Text"}, - {"fieldname": "invoice_reference", "label": "Invoice Reference", "fieldtype": "Data", "read_only": 1} + "creation": "2026-08-05 06:22:58.563978", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "order", + "customer", + "delivery_address", + "column_break_1", + "quantity_dispatched", + "boxes_used", + "box_batch_number", + "tape_rolls_used", + "rope_meters_used", + "section_break_1", + "transport_mode", + "driver_name", + "vehicle_transport_id", + "column_break_2", + "dispatch_datetime", + "fuel_liters", + "quality_check_pass", + "quality_check_notes", + "invoice_reference", + "naming_series", + "amended_from" ], + "fields": [ + { + "fieldname": "order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Order", + "options": "Order Patch", + "reqd": 1 + }, + { + "fetch_from": "order.customer", + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fieldname": "delivery_address", + "fieldtype": "Small Text", + "label": "Delivery Address" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "quantity_dispatched", + "fieldtype": "Int", + "label": "Quantity Dispatched (trays)", + "reqd": 1 + }, + { + "fieldname": "boxes_used", + "fieldtype": "Int", + "label": "Boxes Used" + }, + { + "fieldname": "box_batch_number", + "fieldtype": "Data", + "label": "Box Batch Number" + }, + { + "fieldname": "tape_rolls_used", + "fieldtype": "Float", + "label": "Tape Rolls Used" + }, + { + "fieldname": "rope_meters_used", + "fieldtype": "Float", + "label": "Rope Meters Used" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Transport" + }, + { + "fieldname": "transport_mode", + "fieldtype": "Select", + "label": "Transport Mode", + "options": "Company Vehicle\nThird-Party Courier" + }, + { + "fieldname": "driver_name", + "fieldtype": "Data", + "label": "Driver Name" + }, + { + "fieldname": "vehicle_transport_id", + "fieldtype": "Data", + "label": "Vehicle / Transport ID" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Now", + "fieldname": "dispatch_datetime", + "fieldtype": "Datetime", + "label": "Dispatch Date & Time", + "reqd": 1 + }, + { + "fieldname": "fuel_liters", + "fieldtype": "Float", + "label": "Fuel Consumed (L)" + }, + { + "fieldname": "quality_check_pass", + "fieldtype": "Select", + "label": "Final QC Result", + "options": "Pass\nFail", + "reqd": 1 + }, + { + "fieldname": "quality_check_notes", + "fieldtype": "Small Text", + "label": "QC Notes" + }, + { + "fieldname": "invoice_reference", + "fieldtype": "Data", + "label": "Invoice Reference", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "DSR-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Dispatch Record", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2026-08-06 03:23:16.670402", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Dispatch Record", + "owner": "Administrator", "permissions": [ - {"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} - ] -} + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Admin", + "submit": 1, + "write": 1 + }, + { + "create": 1, + "read": 1, + "role": "Stock Manager", + "submit": 1, + "write": 1 + }, + { + "read": 1, + "report": 1, + "role": "Accountant" + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/dispatch_record/test_dispatch_record.py b/agri_seedling/agri_seedling/doctype/dispatch_record/test_dispatch_record.py new file mode 100644 index 0000000..e2326f7 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/dispatch_record/test_dispatch_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 TestDispatchRecord(FrappeTestCase): + pass 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 index a7c46b7..675509b 100644 --- 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 @@ -97,7 +97,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-07-31 01:52:32.575812", + "modified": "2026-08-06 03:29:29.595002", "modified_by": "Administrator", "module": "Agri Seedling", "name": "Media Mix Batch", @@ -114,6 +114,18 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", diff --git a/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json index b07c3be..e86ab8b 100644 --- a/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json +++ b/agri_seedling/agri_seedling/doctype/order_patch/order_patch.json @@ -13,6 +13,7 @@ "requested_delivery_date", "greenhouse_zone", "worker_team", + "worker", "estimated_readiness_date", "priority_level", "special_handling_notes", @@ -59,6 +60,12 @@ "label": "Worker Team", "options": "Worker Profile" }, + { + "fieldname": "worker", + "fieldtype": "Link", + "label": "Worker", + "options": "Worker Profile" + }, { "fieldname": "estimated_readiness_date", "fieldtype": "Date", @@ -103,6 +110,13 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "write": 1 } ], "row_format": "Dynamic", diff --git a/agri_seedling/agri_seedling/doctype/pest/__init__.py b/agri_seedling/agri_seedling/doctype/pest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/pest/pest.js b/agri_seedling/agri_seedling/doctype/pest/pest.js new file mode 100644 index 0000000..d6416ba --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/pest/pest.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("Pest", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/pest/pest.json b/agri_seedling/agri_seedling/doctype/pest/pest.json new file mode 100644 index 0000000..a9aa101 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/pest/pest.json @@ -0,0 +1,87 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:pest_name", + "creation": "2026-08-06 03:11:41.797697", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "pest_name", + "pest_type", + "notes" + ], + "fields": [ + { + "fieldname": "pest_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Pest / Disease Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "pest_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Pest\nDisease\nNutrient Deficiency\nOther" + }, + { + "fieldname": "notes", + "fieldtype": "Small Text", + "label": "Notes" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-06 03:11:41.797697", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Pest", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Admin", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/pest/pest.py b/agri_seedling/agri_seedling/doctype/pest/pest.py new file mode 100644 index 0000000..21f528f --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/pest/pest.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 Pest(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/pest/test_pest.py b/agri_seedling/agri_seedling/doctype/pest/test_pest.py new file mode 100644 index 0000000..92280cc --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/pest/test_pest.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 TestPest(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/planting_record/planting_record.js b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.js new file mode 100644 index 0000000..6e07e8c --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/planting_record/planting_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("Planting Record", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json index bed8928..fa1a3c4 100644 --- a/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json +++ b/agri_seedling/agri_seedling/doctype/planting_record/planting_record.json @@ -1,33 +1,171 @@ { - "doctype": "DocType", - "name": "Planting Record", - "module": "Agri Seedling", - "istable": 0, - "issingle": 0, - "is_submittable": 1, + "actions": [], "autoname": "naming_series:", - "naming_series": "PLT-.YYYY.-.#####", - "track_changes": 1, - "fields": [ - {"fieldname": "planting_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, - {"fieldname": "tray_batch_id", "label": "Tray Batch ID", "fieldtype": "Data", "reqd": 1, "in_list_view": 1}, - {"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety", "reqd": 1}, - {"fieldname": "seed_quantity", "label": "Seed Quantity Used", "fieldtype": "Float", "reqd": 1}, - {"fieldname": "seed_lot_number", "label": "Seed Lot / Batch Number", "fieldtype": "Data", "reqd": 1}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "media_batch", "label": "Media Batch", "fieldtype": "Link", "options": "Media Mix Batch"}, - {"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1}, - {"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1}, - {"fieldname": "team_size", "label": "Team Size", "fieldtype": "Int"}, - {"fieldname": "section_break_1", "label": "Tray Configuration", "fieldtype": "Section Break"}, - {"fieldname": "tray_count", "label": "Tray Count", "fieldtype": "Int", "reqd": 1}, - {"fieldname": "cell_rows", "label": "Cell Rows", "fieldtype": "Int"}, - {"fieldname": "cell_columns", "label": "Cell Columns", "fieldtype": "Int"}, - {"fieldname": "seeds_per_cell", "label": "Seeds per Cell", "fieldtype": "Int"}, - {"fieldname": "expected_germination_date", "label": "Expected Germination Date", "fieldtype": "Date"} + "creation": "2026-08-05 06:22:56.549409", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "planting_datetime", + "tray_batch_id", + "seedling_variety", + "seed_quantity", + "seed_lot_number", + "column_break_1", + "media_batch", + "zone", + "worker", + "team_size", + "section_break_1", + "tray_count", + "cell_rows", + "cell_columns", + "seeds_per_cell", + "expected_germination_date", + "naming_series", + "amended_from" ], + "fields": [ + { + "default": "Now", + "fieldname": "planting_datetime", + "fieldtype": "Datetime", + "label": "Date & Time", + "reqd": 1 + }, + { + "fieldname": "tray_batch_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Tray Batch ID", + "reqd": 1 + }, + { + "fieldname": "seedling_variety", + "fieldtype": "Link", + "label": "Crop", + "options": "Crop", + "reqd": 1 + }, + { + "fieldname": "seed_quantity", + "fieldtype": "Float", + "label": "Seed Quantity Used", + "reqd": 1 + }, + { + "fieldname": "seed_lot_number", + "fieldtype": "Data", + "label": "Seed Lot / Batch Number", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "media_batch", + "fieldtype": "Link", + "label": "Media Batch", + "options": "Media Mix Batch" + }, + { + "fieldname": "zone", + "fieldtype": "Link", + "label": "Greenhouse Zone", + "options": "Greenhouse Zone", + "reqd": 1 + }, + { + "fieldname": "worker", + "fieldtype": "Link", + "label": "Worker", + "options": "Worker Profile", + "reqd": 1 + }, + { + "fieldname": "team_size", + "fieldtype": "Int", + "label": "Team Size" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Tray Configuration" + }, + { + "fieldname": "tray_count", + "fieldtype": "Int", + "label": "Tray Count", + "reqd": 1 + }, + { + "fieldname": "cell_rows", + "fieldtype": "Int", + "label": "Cell Rows" + }, + { + "fieldname": "cell_columns", + "fieldtype": "Int", + "label": "Cell Columns" + }, + { + "fieldname": "seeds_per_cell", + "fieldtype": "Int", + "label": "Seeds per Cell" + }, + { + "fieldname": "expected_germination_date", + "fieldtype": "Date", + "label": "Expected Germination Date" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "PLR-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Planting Record", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2026-08-06 03:22:07.512082", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Planting Record", + "owner": "Administrator", "permissions": [ - {"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, - {"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1} - ] -} + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Admin", + "submit": 1, + "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "submit": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/planting_record/test_planting_record.py b/agri_seedling/agri_seedling/doctype/planting_record/test_planting_record.py new file mode 100644 index 0000000..76fb5b5 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/planting_record/test_planting_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 TestPlantingRecord(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/resource_consumption_log/__init__.py b/agri_seedling/agri_seedling/doctype/resource_consumption_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.json b/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.json new file mode 100644 index 0000000..65b8a7d --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.json @@ -0,0 +1,117 @@ +{ + "doctype": "DocType", + "name": "Resource Consumption Log", + "module": "Agri Seedling", + "istable": 0, + "issingle": 0, + "is_submittable": 0, + "autoname": "hash", + "track_changes": 0, + "sort_field": "logged_on", + "sort_order": "DESC", + "fields": [ + { + "fieldname": "resource", + "label": "Resource", + "fieldtype": "Link", + "options": "Resource Inventory", + "reqd": 1, + "in_list_view": 1, + "in_standard_filter": 1 + }, + { + "fieldname": "category", + "label": "Category", + "fieldtype": "Data", + "fetch_from": "resource.category", + "read_only": 1, + "in_list_view": 1, + "in_standard_filter": 1 + }, + { + "fieldname": "unit_of_measure", + "label": "Unit", + "fieldtype": "Data", + "fetch_from": "resource.unit_of_measure", + "read_only": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "quantity", + "label": "Quantity Used", + "fieldtype": "Float", + "reqd": 1, + "in_list_view": 1, + "description": "Positive = consumed. Negative = reversed (e.g. on cancellation)." + }, + { + "fieldname": "logged_on", + "label": "Logged On", + "fieldtype": "Datetime", + "default": "Now", + "reqd": 1, + "in_list_view": 1, + "in_standard_filter": 1 + }, + { + "fieldname": "zone", + "label": "Zone", + "fieldtype": "Link", + "options": "Greenhouse Zone", + "in_standard_filter": 1 + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "source_doctype", + "label": "Source Doctype", + "fieldtype": "Link", + "options": "DocType", + "in_standard_filter": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "source_name", + "label": "Source Record", + "fieldtype": "Dynamic Link", + "options": "source_doctype" + } + ], + "permissions": [ + { + "role": "Admin", + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + "report": 1, + "export": 1 + }, + { + "role": "Stock Manager", + "read": 1, + "report": 1, + "export": 1 + }, + { + "role": "Inventory Manager", + "read": 1, + "report": 1, + "export": 1 + }, + { + "role": "Accountant", + "read": 1, + "report": 1, + "export": 1 + } + ] +} diff --git a/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.py b/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.py new file mode 100644 index 0000000..01cd06a --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_consumption_log/resource_consumption_log.py @@ -0,0 +1,6 @@ +import frappe +from frappe.model.document import Document + + +class ResourceConsumptionLog(Document): + pass diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.js b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.js new file mode 100644 index 0000000..860896f --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.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("Resource Inventory", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json index e58597e..c6566f9 100644 --- a/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json +++ b/agri_seedling/agri_seedling/doctype/resource_inventory/resource_inventory.json @@ -1,39 +1,161 @@ { - "doctype": "DocType", - "name": "Resource Inventory", - "module": "Agri Seedling", - "custom": 0, - "istable": 0, - "issingle": 0, - "is_submittable": 0, + "actions": [], "autoname": "field:resource_name", - "track_changes": 1, - "fields": [ - {"fieldname": "resource_name", "label": "Resource Name", "fieldtype": "Data", "reqd": 1, "unique": 1, "in_list_view": 1}, - {"fieldname": "category", "label": "Category", "fieldtype": "Select", "in_list_view": 1, "reqd": 1, - "options": "Labor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel"}, - {"fieldname": "unit_of_measure", "label": "Unit of Measure", "fieldtype": "Select", - "options": "Person-hours\nLiters\nGrams\nCount\nkWh\nRolls\nMeters\nTrips\nKilometers"}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "current_stock", "label": "Current Stock", "fieldtype": "Float", "in_list_view": 1}, - {"fieldname": "reorder_threshold", "label": "Reorder Threshold", "fieldtype": "Float", - "description": "e.g. 7-day supply equivalent for fungicides, 50 units for boxes — see Appendix C."}, - {"fieldname": "reorder_quantity", "label": "Standard Reorder Quantity", "fieldtype": "Float"}, - {"fieldname": "alert_priority", "label": "Alert Priority", "fieldtype": "Select", "options": "LOW\nNORMAL\nHIGH\nCRITICAL", "default": "NORMAL"}, - {"fieldname": "section_break_1", "label": "Batch / Traceability", "fieldtype": "Section Break"}, - {"fieldname": "batch_number", "label": "Batch / Lot Number", "fieldtype": "Data"}, - {"fieldname": "supplier", "label": "Supplier", "fieldtype": "Data"}, - {"fieldname": "expiry_date", "label": "Expiry Date", "fieldtype": "Date"}, - {"fieldname": "registration_number", "label": "Chemical Registration Number", "fieldtype": "Data", - "description": "Required for fungicides/pesticides per Sec 9.3 compliance."}, - {"fieldname": "cost_rate", "label": "Cost Rate (per unit)", "fieldtype": "Currency"}, - {"fieldname": "approval_required_role", "label": "Approval Required From", "fieldtype": "Select", - "options": "\nStock Manager\nAdmin\nAdmin + Stock Manager"} + "creation": "2026-08-05 06:22:52.194635", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "resource_name", + "category", + "unit_of_measure", + "column_break_1", + "current_stock", + "reorder_threshold", + "reorder_quantity", + "alert_priority", + "section_break_1", + "batch_number", + "supplier", + "expiry_date", + "registration_number", + "cost_rate", + "approval_required_role" ], + "fields": [ + { + "fieldname": "resource_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Resource Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "category", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Category", + "options": "Labor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel", + "reqd": 1 + }, + { + "fieldname": "unit_of_measure", + "fieldtype": "Select", + "label": "Unit of Measure", + "options": "Person-hours\nLiters\nGrams\nCount\nkWh\nRolls\nMeters\nTrips\nKilometers" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "current_stock", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Current Stock" + }, + { + "description": "e.g. 7-day supply equivalent for fungicides, 50 units for boxes \u2014 see Appendix C.", + "fieldname": "reorder_threshold", + "fieldtype": "Float", + "label": "Reorder Threshold" + }, + { + "fieldname": "reorder_quantity", + "fieldtype": "Float", + "label": "Standard Reorder Quantity" + }, + { + "default": "NORMAL", + "fieldname": "alert_priority", + "fieldtype": "Select", + "label": "Alert Priority", + "options": "LOW\nNORMAL\nHIGH\nCRITICAL" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Batch / Traceability" + }, + { + "fieldname": "batch_number", + "fieldtype": "Data", + "label": "Batch / Lot Number" + }, + { + "fieldname": "supplier", + "fieldtype": "Data", + "label": "Supplier" + }, + { + "fieldname": "expiry_date", + "fieldtype": "Date", + "label": "Expiry Date" + }, + { + "description": "Required for fungicides/pesticides per Sec 9.3 compliance.", + "fieldname": "registration_number", + "fieldtype": "Data", + "label": "Chemical Registration Number" + }, + { + "fieldname": "cost_rate", + "fieldtype": "Currency", + "label": "Cost Rate (per unit)" + }, + { + "fieldname": "approval_required_role", + "fieldtype": "Select", + "label": "Approval Required From", + "options": "\nStock Manager\nAdmin\nAdmin + Stock Manager" + } + ], + "links": [], + "modified": "2026-08-06 03:29:29.970439", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Resource Inventory", + "owner": "Administrator", "permissions": [ - {"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} - ] -} + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Admin", + "write": 1 + }, + { + "create": 1, + "read": 1, + "role": "Stock Manager", + "write": 1 + }, + { + "create": 1, + "read": 1, + "role": "Inventory Manager", + "write": 1 + }, + { + "read": 1, + "role": "Accountant" + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/resource_inventory/test_resource_inventory.py b/agri_seedling/agri_seedling/doctype/resource_inventory/test_resource_inventory.py new file mode 100644 index 0000000..37199b6 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/resource_inventory/test_resource_inventory.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 TestResourceInventory(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.js b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.js new file mode 100644 index 0000000..00ae07d --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.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("Scouting Report", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json index 54828cd..e608861 100644 --- a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json +++ b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.json @@ -1,38 +1,186 @@ { - "doctype": "DocType", - "name": "Scouting Report", - "module": "Agri Seedling", - "custom": 0, - "istable": 0, - "issingle": 0, - "is_submittable": 1, + "actions": [], "autoname": "naming_series:", - "naming_series": "SCT-.YYYY.-.#####", - "track_changes": 1, - "fields": [ - {"fieldname": "inspection_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now", "in_list_view": 1}, - {"fieldname": "zone", "label": "Greenhouse Zone", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1}, - {"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "pest_disease_type", "label": "Pest / Disease Type", "fieldtype": "Select", "in_list_view": 1, - "options": "\nAphids\nWhitefly\nSpider Mites\nDamping Off\nPowdery Mildew\nBacterial Blight\nNutrient Deficiency\nOther"}, - {"fieldname": "severity", "label": "Severity (1-5)", "fieldtype": "Int", "reqd": 1, "in_list_view": 1, - "description": "1 = trace, 5 = severe outbreak. Severity >= 3 triggers a Critical push alert to Admin and Stock Manager."}, - {"fieldname": "seedling_variety", "label": "Seedling Variety", "fieldtype": "Link", "options": "Seed Variety"}, - {"fieldname": "percent_trays_affected", "label": "Est. % Trays Affected", "fieldtype": "Percent"}, - {"fieldname": "section_break_1", "label": "Evidence", "fieldtype": "Section Break"}, - {"fieldname": "photo", "label": "Photo", "fieldtype": "Attach Image", - "description": "Required (min 1) when severity >= 3 — enforced in validate()."}, - {"fieldname": "gps_latitude", "label": "GPS Latitude", "fieldtype": "Float", "read_only": 1}, - {"fieldname": "gps_longitude", "label": "GPS Longitude", "fieldtype": "Float", "read_only": 1}, - {"fieldname": "notes", "label": "Notes / Observations", "fieldtype": "Small Text"}, - {"fieldname": "section_break_2", "label": "Follow-up", "fieldtype": "Section Break"}, - {"fieldname": "recommended_action", "label": "Recommended Action Flag", "fieldtype": "Small Text", "read_only": 1}, - {"fieldname": "escalated", "label": "Escalated to Admin", "fieldtype": "Check", "read_only": 1, "default": "0"} + "creation": "2026-08-05 06:22:54.691986", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "inspection_datetime", + "zone", + "worker", + "column_break_1", + "pest_disease_type", + "severity", + "seedling_variety", + "percent_trays_affected", + "section_break_1", + "photo", + "gps_latitude", + "gps_longitude", + "notes", + "section_break_2", + "recommended_action", + "escalated", + "naming_series", + "amended_from" ], + "fields": [ + { + "default": "Now", + "fieldname": "inspection_datetime", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Date & Time", + "reqd": 1 + }, + { + "fieldname": "zone", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Greenhouse Zone", + "options": "Greenhouse Zone", + "reqd": 1 + }, + { + "fieldname": "worker", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Worker", + "options": "Worker Profile", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "pest_disease_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Pest / Disease", + "options": "Pest" + }, + { + "description": "1 = trace, 5 = severe outbreak. Severity >= 3 triggers a Critical push alert to Admin and Stock Manager.", + "fieldname": "severity", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Severity (1-5)", + "reqd": 1 + }, + { + "fieldname": "seedling_variety", + "fieldtype": "Link", + "label": "Crop", + "options": "Crop" + }, + { + "fieldname": "percent_trays_affected", + "fieldtype": "Percent", + "label": "Est. % Trays Affected" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Evidence" + }, + { + "description": "Required (min 1) when severity >= 3 \u2014 enforced in validate().", + "fieldname": "photo", + "fieldtype": "Attach Image", + "label": "Photo" + }, + { + "fieldname": "gps_latitude", + "fieldtype": "Float", + "label": "GPS Latitude", + "read_only": 1 + }, + { + "fieldname": "gps_longitude", + "fieldtype": "Float", + "label": "GPS Longitude", + "read_only": 1 + }, + { + "fieldname": "notes", + "fieldtype": "Small Text", + "label": "Notes / Observations" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Follow-up" + }, + { + "fieldname": "recommended_action", + "fieldtype": "Small Text", + "label": "Recommended Action Flag", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "escalated", + "fieldtype": "Check", + "label": "Escalated to Admin", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "SCT-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Scouting Report", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2026-08-06 03:20:01.941408", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Scouting Report", + "owner": "Administrator", "permissions": [ - {"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} - ] -} + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Admin", + "submit": 1, + "write": 1 + }, + { + "create": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "submit": 1, + "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "submit": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py index ce17227..dd0121b 100644 --- a/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py +++ b/agri_seedling/agri_seedling/doctype/scouting_report/scouting_report.py @@ -5,6 +5,11 @@ from frappe.model.document import Document class ScoutingReport(Document): def validate(self): + # Incoming severity can arrive as a string (e.g. from mobile form + # inputs); coerce it once here so every comparison below is safe. + if self.severity not in (None, ""): + self.severity = frappe.utils.cint(self.severity) + if self.severity and self.severity >= 3 and not self.photo: frappe.throw(_("At least one photo is required when severity is 3 or higher.")) diff --git a/agri_seedling/agri_seedling/doctype/scouting_report/test_scouting_report.py b/agri_seedling/agri_seedling/doctype/scouting_report/test_scouting_report.py new file mode 100644 index 0000000..b9faeff --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/scouting_report/test_scouting_report.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 TestScoutingReport(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json index 08b329e..e7a17b5 100644 --- a/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json +++ b/agri_seedling/agri_seedling/doctype/seed_variety/seed_variety.json @@ -40,7 +40,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-07-31 01:41:09.884281", + "modified": "2026-08-06 03:29:29.238696", "modified_by": "Administrator", "module": "Agri Seedling", "name": "Seed Variety", @@ -57,6 +57,18 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", 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 index d7fe2b1..7c2d60e 100644 --- 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 @@ -10,7 +10,7 @@ "date_time", "greenhouse_zone", "chemical_category", - "product_name", + "product", "registration_number", "concentration", "volume_prepared_liters", @@ -24,7 +24,8 @@ "morning_spray_reference", "pest_status_change", "weather_update", - "decontamination_confirmed" + "decontamination_confirmed", + "target_pest" ], "fields": [ { @@ -46,11 +47,6 @@ "label": "Chemical Category", "options": "Fungicide\nPesticide\nFoliar" }, - { - "fieldname": "product_name", - "fieldtype": "Data", - "label": "Product Name" - }, { "fieldname": "registration_number", "fieldtype": "Data", @@ -129,12 +125,24 @@ "fieldname": "decontamination_confirmed", "fieldtype": "Check", "label": "Decontamination Confirmed" + }, + { + "fieldname": "product", + "fieldtype": "Link", + "label": "Chemical", + "options": "Chemical" + }, + { + "fieldname": "target_pest", + "fieldtype": "Link", + "label": "Target Pest", + "options": "Pest" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-07-31 01:53:08.496728", + "modified": "2026-08-06 03:21:30.352449", "modified_by": "Administrator", "module": "Agri Seedling", "name": "Spray Log Evening", diff --git a/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.js b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.js new file mode 100644 index 0000000..d27e31d --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_morning/spray_log_morning.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 Morning", { +// refresh(frm) { + +// }, +// }); 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 index ee3f9db..45aa2fb 100644 --- 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 @@ -1,47 +1,242 @@ { - "doctype": "DocType", - "name": "Spray Log Morning", - "module": "Agri Seedling", - "custom": 0, - "istable": 0, - "issingle": 0, - "is_submittable": 1, + "actions": [], "autoname": "naming_series:", - "naming_series": "SPRM-.YYYY.-.#####", - "track_changes": 1, - "allow_on_submit": [], - "fields": [ - {"fieldname": "spray_datetime", "label": "Date & Time", "fieldtype": "Datetime", "reqd": 1, "default": "Now"}, - {"fieldname": "zone", "label": "Greenhouse Zone / Tray Range", "fieldtype": "Link", "options": "Greenhouse Zone", "reqd": 1, "in_list_view": 1}, - {"fieldname": "worker", "label": "Worker", "fieldtype": "Link", "options": "Worker Profile", "reqd": 1, "in_list_view": 1}, - {"fieldname": "column_break_1", "fieldtype": "Column Break"}, - {"fieldname": "chemical_category", "label": "Chemical Category", "fieldtype": "Select", "reqd": 1, "in_list_view": 1, - "options": "Fungicide\nPesticide\nFoliar"}, - {"fieldname": "product", "label": "Product", "fieldtype": "Link", "options": "Resource Inventory", "reqd": 1}, - {"fieldname": "registration_number", "label": "Registration Number", "fieldtype": "Data", "fetch_from": "product.registration_number", "read_only": 1}, - {"fieldname": "concentration", "label": "Concentration (mL/L or g/L)", "fieldtype": "Float"}, - {"fieldname": "section_break_1", "label": "Volumes", "fieldtype": "Section Break"}, - {"fieldname": "solution_volume_prepared", "label": "Solution Prepared (L)", "fieldtype": "Float", "reqd": 1}, - {"fieldname": "solution_volume_sprayed", "label": "Solution Sprayed (L)", "fieldtype": "Float", "reqd": 1}, - {"fieldname": "water_volume_used", "label": "Water Used for Dilution (L)", "fieldtype": "Float"}, - {"fieldname": "target", "label": "Target (pest / deficiency)", "fieldtype": "Data"}, - {"fieldname": "section_break_2", "label": "PPE Checklist", "fieldtype": "Section Break", - "description": "All four must be checked before this record can be submitted (Sec 9.3)."}, - {"fieldname": "ppe_gloves", "label": "Gloves", "fieldtype": "Check"}, - {"fieldname": "ppe_mask", "label": "Mask / Respirator", "fieldtype": "Check"}, - {"fieldname": "column_break_2", "fieldtype": "Column Break"}, - {"fieldname": "ppe_goggles", "label": "Goggles", "fieldtype": "Check"}, - {"fieldname": "ppe_coveralls", "label": "Coveralls", "fieldtype": "Check"}, - {"fieldname": "section_break_3", "label": "Conditions", "fieldtype": "Section Break"}, - {"fieldname": "ambient_temp_c", "label": "Ambient Temp (°C)", "fieldtype": "Float"}, - {"fieldname": "wind_speed", "label": "Wind Speed", "fieldtype": "Select", "options": "Calm\nLow\nModerate"}, - {"fieldname": "column_break_3", "fieldtype": "Column Break"}, - {"fieldname": "reentry_interval_hours", "label": "Re-Entry Interval (hrs)", "fieldtype": "Float"}, - {"fieldname": "reentry_lock_expires", "label": "Zone Restricted Until", "fieldtype": "Datetime", "read_only": 1}, - {"fieldname": "ppe_non_compliant", "label": "PPE Non-Compliance Flagged", "fieldtype": "Check", "read_only": 1, "default": "0"} + "creation": "2026-08-05 06:22:56.993986", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "spray_datetime", + "zone", + "worker", + "column_break_1", + "chemical_category", + "product", + "registration_number", + "concentration", + "section_break_1", + "solution_volume_prepared", + "solution_volume_sprayed", + "water_volume_used", + "target", + "section_break_2", + "ppe_gloves", + "ppe_mask", + "column_break_2", + "ppe_goggles", + "ppe_coveralls", + "section_break_3", + "ambient_temp_c", + "wind_speed", + "column_break_3", + "reentry_interval_hours", + "reentry_lock_expires", + "ppe_non_compliant", + "naming_series", + "amended_from" ], + "fields": [ + { + "default": "Now", + "fieldname": "spray_datetime", + "fieldtype": "Datetime", + "label": "Date & Time", + "reqd": 1 + }, + { + "fieldname": "zone", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Greenhouse Zone / Tray Range", + "options": "Greenhouse Zone", + "reqd": 1 + }, + { + "fieldname": "worker", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Worker", + "options": "Worker Profile", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "chemical_category", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Chemical Category", + "options": "Fungicide\nPesticide\nFoliar", + "reqd": 1 + }, + { + "fieldname": "product", + "fieldtype": "Link", + "label": "Chemical", + "options": "Chemical", + "reqd": 1 + }, + { + "fetch_from": "product.registration_number", + "fieldname": "registration_number", + "fieldtype": "Data", + "label": "Registration Number", + "read_only": 1 + }, + { + "fieldname": "concentration", + "fieldtype": "Float", + "label": "Concentration (mL/L or g/L)" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Volumes" + }, + { + "fieldname": "solution_volume_prepared", + "fieldtype": "Float", + "label": "Solution Prepared (L)", + "reqd": 1 + }, + { + "fieldname": "solution_volume_sprayed", + "fieldtype": "Float", + "label": "Solution Sprayed (L)", + "reqd": 1 + }, + { + "fieldname": "water_volume_used", + "fieldtype": "Float", + "label": "Water Used for Dilution (L)" + }, + { + "fieldname": "target", + "fieldtype": "Link", + "label": "Target Pest", + "options": "Pest" + }, + { + "description": "All four must be checked before this record can be submitted (Sec 9.3).", + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "PPE Checklist" + }, + { + "default": "0", + "fieldname": "ppe_gloves", + "fieldtype": "Check", + "label": "Gloves" + }, + { + "default": "0", + "fieldname": "ppe_mask", + "fieldtype": "Check", + "label": "Mask / Respirator" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "ppe_goggles", + "fieldtype": "Check", + "label": "Goggles" + }, + { + "default": "0", + "fieldname": "ppe_coveralls", + "fieldtype": "Check", + "label": "Coveralls" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Conditions" + }, + { + "fieldname": "ambient_temp_c", + "fieldtype": "Float", + "label": "Ambient Temp (\u00b0C)" + }, + { + "fieldname": "wind_speed", + "fieldtype": "Select", + "label": "Wind Speed", + "options": "Calm\nLow\nModerate" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "reentry_interval_hours", + "fieldtype": "Float", + "label": "Re-Entry Interval (hrs)" + }, + { + "fieldname": "reentry_lock_expires", + "fieldtype": "Datetime", + "label": "Zone Restricted Until", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "ppe_non_compliant", + "fieldtype": "Check", + "label": "PPE Non-Compliance Flagged", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "SLM-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Spray Log Morning", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2026-08-06 03:20:45.930927", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Spray Log Morning", + "owner": "Administrator", "permissions": [ - {"role": "Admin", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1}, - {"role": "Field Worker", "read": 1, "create": 1, "submit": 1, "if_owner": 1} - ] -} + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Admin", + "submit": 1, + "write": 1 + }, + { + "create": 1, + "if_owner": 1, + "read": 1, + "role": "Field Worker", + "submit": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/spray_log_morning/test_spray_log_morning.py b/agri_seedling/agri_seedling/doctype/spray_log_morning/test_spray_log_morning.py new file mode 100644 index 0000000..f2b3492 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/spray_log_morning/test_spray_log_morning.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 TestSprayLogMorning(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/tray_type/__init__.py b/agri_seedling/agri_seedling/doctype/tray_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/doctype/tray_type/test_tray_type.py b/agri_seedling/agri_seedling/doctype/tray_type/test_tray_type.py new file mode 100644 index 0000000..506413e --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_type/test_tray_type.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 TestTrayType(FrappeTestCase): + pass diff --git a/agri_seedling/agri_seedling/doctype/tray_type/tray_type.js b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.js new file mode 100644 index 0000000..6ab3aaa --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.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 Type", { +// refresh(frm) { + +// }, +// }); diff --git a/agri_seedling/agri_seedling/doctype/tray_type/tray_type.json b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.json new file mode 100644 index 0000000..20fd0f0 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:tray_type_name", + "creation": "2026-08-06 03:13:09.593649", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "tray_type_name", + "cell_count", + "material" + ], + "fields": [ + { + "fieldname": "tray_type_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Tray Type Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "cell_count", + "fieldtype": "Int", + "label": "Cell Count" + }, + { + "fieldname": "material", + "fieldtype": "Data", + "label": "Material" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-06 03:13:09.593649", + "modified_by": "Administrator", + "module": "Agri Seedling", + "name": "Tray Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Admin", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Field Worker", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/agri_seedling/agri_seedling/doctype/tray_type/tray_type.py b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.py new file mode 100644 index 0000000..56c8ce9 --- /dev/null +++ b/agri_seedling/agri_seedling/doctype/tray_type/tray_type.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 TrayType(Document): + 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 index 700671a..a7a7564 100644 --- a/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.js +++ b/agri_seedling/agri_seedling/doctype/worker_profile/worker_profile.js @@ -1,8 +1,18 @@ -// Copyright (c) 2026, Agri Seedling Operations Team and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Worker Profile", { -// refresh(frm) { - -// }, -// }); +frappe.ui.form.on('Worker Profile', { + refresh(frm) { + if (!frm.is_new()) { + frm.add_custom_button('Generate QR Badge', () => { + frappe.call({ + method: 'agri_seedling.agri_seedling.api.worker_qr.generate_worker_qr', + args: { worker_id: frm.doc.name }, + callback: (r) => { + if (r.message) { + frappe.msgprint('QR badge generated.'); + frm.reload_doc(); + } + } + }); + }); + } + } +}); diff --git a/agri_seedling/agri_seedling/print_format/worker_id_badge/worker_id_badge.json b/agri_seedling/agri_seedling/print_format/worker_id_badge/worker_id_badge.json new file mode 100644 index 0000000..baa579c --- /dev/null +++ b/agri_seedling/agri_seedling/print_format/worker_id_badge/worker_id_badge.json @@ -0,0 +1,23 @@ +{ + "align_labels_right": 0, + "css": ".badge-card { width: 3.4in; height: 2.1in; border: 2px solid #2E7D6B; border-radius: 10px; padding: 12px 14px; font-family: 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; } .badge-header { font-size: 9px; letter-spacing: 1.5px; color: #6B7280; text-transform: uppercase; } .badge-body { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; } .badge-name { font-size: 16px; font-weight: 700; color: #1F3B57; margin: 0 0 2px 0; } .badge-id { font-size: 11px; color: #333; margin: 0; } .badge-phone { font-size: 10px; color: #6B7280; margin-top: 2px; } .badge-qr img { width: 1.15in; height: 1.15in; } .badge-footer { font-size: 8px; color: #9CA3AF; text-align: center; margin-top: 4px; }", + "custom_format": 1, + "disabled": 0, + "doc_type": "Worker Profile", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "
\n
Agri Seedling · Field Worker ID
\n
\n
\n

{{ doc.full_name }}

\n

ID: {{ doc.name }}

\n

{{ doc.phone_number }}

\n
\n
\n {% if doc.qr_image %}\n \n {% endif %}\n
\n
\n
Scan to log field activity — do not share this badge
\n
", + "line_breaks": 0, + "margin_bottom": 5, + "margin_left": 5, + "margin_right": 5, + "margin_top": 5, + "module": "Agri Seedling", + "name": "Worker ID Badge", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "standard": "Yes" +} diff --git a/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.json b/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.json new file mode 100644 index 0000000..1c70729 --- /dev/null +++ b/agri_seedling/agri_seedling/report/daily_resource_consumption/daily_resource_consumption.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2026-08-05 00:00:00.000000", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "module": "Agri Seedling", + "name": "Daily Resource Consumption", + "prepared_report": 0, + "ref_doctype": "Resource Inventory", + "report_name": "Daily Resource Consumption", + "report_type": "Script Report", + "roles": [ + {"role": "Admin"}, + {"role": "Stock Manager"}, + {"role": "Inventory Manager"}, + {"role": "Accountant"} + ] +} diff --git a/agri_seedling/agri_seedling/report/daily_resource_usage_summary/__init__.py b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.js b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.js new file mode 100644 index 0000000..5409073 --- /dev/null +++ b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.js @@ -0,0 +1,42 @@ +frappe.query_reports["Daily Resource Usage Summary"] = { + filters: [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "category", + label: __("Category"), + fieldtype: "Select", + options: "\nLabor\nWater\nFungicides\nPesticides\nSeeds\nFoliar Feeds\nElectricity\nBoxes\nTapes\nRope\nTransport\nJigs\nFuel", + }, + { + fieldname: "resource", + label: __("Resource"), + fieldtype: "Link", + options: "Resource Inventory", + }, + { + fieldname: "zone", + label: __("Zone"), + fieldtype: "Link", + options: "Greenhouse Zone", + }, + { + fieldname: "group_by_zone", + label: __("Break Down by Zone"), + fieldtype: "Check", + default: 0, + }, + ], +}; diff --git a/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.json b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.json new file mode 100644 index 0000000..f25369e --- /dev/null +++ b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2026-08-07 00:00:00.000000", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "module": "Agri Seedling", + "name": "Daily Resource Usage Summary", + "prepared_report": 0, + "ref_doctype": "Resource Consumption Log", + "report_name": "Daily Resource Usage Summary", + "report_type": "Script Report", + "roles": [ + {"role": "Admin"}, + {"role": "Stock Manager"}, + {"role": "Inventory Manager"}, + {"role": "Accountant"} + ] +} diff --git a/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.py b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.py new file mode 100644 index 0000000..6f87259 --- /dev/null +++ b/agri_seedling/agri_seedling/report/daily_resource_usage_summary/daily_resource_usage_summary.py @@ -0,0 +1,61 @@ +import frappe +from frappe import _ +from frappe.utils import getdate, today + + +def execute(filters=None): + filters = frappe._dict(filters or {}) + if not filters.from_date: + filters.from_date = today() + if not filters.to_date: + filters.to_date = today() + + columns = get_columns() + data = get_data(filters) + return columns, data + + +def get_columns(): + return [ + {"label": _("Resource"), "fieldname": "resource", "fieldtype": "Link", "options": "Resource Inventory", "width": 200}, + {"label": _("Category"), "fieldname": "category", "fieldtype": "Data", "width": 130}, + {"label": _("Total Used"), "fieldname": "total_used", "fieldtype": "Float", "width": 120}, + {"label": _("Unit"), "fieldname": "unit_of_measure", "fieldtype": "Data", "width": 100}, + {"label": _("Events"), "fieldname": "event_count", "fieldtype": "Int", "width": 90}, + {"label": _("Zone"), "fieldname": "zone", "fieldtype": "Link", "options": "Greenhouse Zone", "width": 130}, + ] + + +def get_data(filters): + conditions = ["logged_on >= %(from_date)s", "logged_on < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)"] + values = {"from_date": getdate(filters.from_date), "to_date": getdate(filters.to_date)} + + if filters.get("category"): + conditions.append("category = %(category)s") + values["category"] = filters.category + + if filters.get("zone"): + conditions.append("zone = %(zone)s") + values["zone"] = filters.zone + + if filters.get("resource"): + conditions.append("resource = %(resource)s") + values["resource"] = filters.resource + + group_by_zone = ", zone" if filters.get("group_by_zone") else "" + select_zone = ", zone" if filters.get("group_by_zone") else ", '' as zone" + + query = f""" + SELECT + resource, + category, + unit_of_measure, + SUM(quantity) AS total_used, + COUNT(*) AS event_count + {select_zone} + FROM `tabResource Consumption Log` + WHERE {" AND ".join(conditions)} + GROUP BY resource, category, unit_of_measure {group_by_zone} + ORDER BY category, total_used DESC + """ + return frappe.db.sql(query, values, as_dict=True) diff --git a/agri_seedling/agri_seedling/utils/stock.py b/agri_seedling/agri_seedling/utils/stock.py index 8d19b7c..5923d8c 100644 --- a/agri_seedling/agri_seedling/utils/stock.py +++ b/agri_seedling/agri_seedling/utils/stock.py @@ -1,23 +1,51 @@ import frappe from frappe import _ +from frappe.utils import now_datetime def deduct_spray_resources(doc, method=None): - if doc.product: - _deduct(doc.product, doc.solution_volume_sprayed or 0) + if not doc.product: + return + resource = frappe.db.get_value("Chemical", doc.product, "linked_resource") + if resource: + _deduct(resource, doc.solution_volume_sprayed or 0, source_doc=doc) + else: + _warn_unlinked("Chemical", doc.product, doc) def reverse_spray_resources(doc, method=None): - if doc.product: - _deduct(doc.product, -(doc.solution_volume_sprayed or 0)) + if not doc.product: + return + resource = frappe.db.get_value("Chemical", doc.product, "linked_resource") + if resource: + _deduct(resource, -(doc.solution_volume_sprayed or 0), source_doc=doc) def deduct_seed_stock(doc, method=None): - seed_resource = frappe.db.get_value( - "Resource Inventory", {"category": "Seeds", "resource_name": ["like", f"%{doc.seedling_variety}%"]}, "name" + if not doc.seedling_variety: + return + resource = frappe.db.get_value("Crop", doc.seedling_variety, "linked_resource") + if resource: + _deduct(resource, doc.seed_quantity or 0, source_doc=doc) + else: + _warn_unlinked("Crop", doc.seedling_variety, doc) + + +def _warn_unlinked(entity_doctype, entity_name, source_doc): + """A Chemical/Crop with no linked_resource means stock silently does not + get deducted — that used to fail completely silently. Now it alerts + Stock Manager + Admin instead, and skips the deduction.""" + from agri_seedling.agri_seedling.utils.notifications import _send_push, _users_with_role + + recipients = _users_with_role(["Stock Manager", "Admin"]) + _send_push( + recipients, + _( + "{0} '{1}' has no linked Resource Inventory item — stock was NOT deducted for {2} {3}. " + "Set its Linked Stock Item to fix this." + ).format(entity_doctype, entity_name, source_doc.doctype, source_doc.name), + priority="HIGH", ) - if seed_resource: - _deduct(seed_resource, doc.seed_quantity or 0) def deduct_dispatch_resources(doc, method=None): @@ -31,17 +59,34 @@ def deduct_dispatch_resources(doc, method=None): if qty: resource = frappe.db.get_value("Resource Inventory", {"category": category}, "name") if resource: - _deduct(resource, qty) + _deduct(resource, qty, source_doc=doc) -def _deduct(resource_name, qty): +def _deduct(resource_name, qty, source_doc=None): if not qty: return current = frappe.db.get_value("Resource Inventory", resource_name, "current_stock") or 0 frappe.db.set_value("Resource Inventory", resource_name, "current_stock", current - qty) + _log_consumption(resource_name, qty, source_doc) check_low_stock_threshold_by_name(resource_name) +def _log_consumption(resource_name, qty, source_doc=None): + """Write one Resource Consumption Log row per deduction/reversal event, + so daily usage can be summed later — current_stock only tells us what's + left, not what happened.""" + log = frappe.new_doc("Resource Consumption Log") + log.resource = resource_name + log.quantity = qty + log.logged_on = now_datetime() + if source_doc is not None: + log.source_doctype = source_doc.doctype + log.source_name = source_doc.name + if source_doc.meta.has_field("zone"): + log.zone = source_doc.zone + log.insert(ignore_permissions=True) + + def check_low_stock_threshold(doc, method=None): check_low_stock_threshold_by_name(doc.name) diff --git a/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json b/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json index a086790..e7d61b2 100644 --- a/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json +++ b/agri_seedling/agri_seedling/workspace/agri_seedling/agri_seedling.json @@ -1,6 +1,6 @@ { "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}}]", + "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}}, {\"id\": \"shortcut_daily_resource_consumption\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Daily Resource Consumption\", \"col\": 4}}, {\"id\": \"shortcut_resource_consumption_log\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Resource Consumption Log\", \"col\": 3}}, {\"id\": \"shortcut_daily_resource_usage_summary\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Daily Resource Usage Summary\", \"col\": 4}}, {\"id\": \"XsxJFHB6xR\", \"type\": \"header\", \"data\": {\"text\": \"\", \"col\": 12}}, {\"id\": \"027be923d5\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Crop\", \"col\": 3}}, {\"id\": \"ccb0706ac7\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Pest\", \"col\": 3}}, {\"id\": \"92ab0d4937\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer\", \"col\": 3}}, {\"id\": \"d0c7f84e66\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chemical\", \"col\": 3}}, {\"id\": \"ab4828a739\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Tray Type\", \"col\": 3}}]", "creation": "2026-07-31 01:37:26.854909", "custom_blocks": [], "docstatus": 0, @@ -11,7 +11,7 @@ "is_hidden": 0, "label": "Agri Seedling", "links": [], - "modified": "2026-07-31 01:55:10.299652", + "modified": "2026-08-07 10:33:14.092545", "modified_by": "Administrator", "module": "Agri Seedling", "name": "Agri Seedling", @@ -123,6 +123,56 @@ "label": "Tray Recycling Record", "link_to": "Tray Recycling Record", "type": "DocType" + }, + { + "doc_view": "", + "label": "Daily Resource Consumption", + "link_to": "Daily Resource Consumption", + "report_ref_doctype": "Resource Inventory", + "type": "Report" + }, + { + "doc_view": "List", + "label": "Crop", + "link_to": "Crop", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Pest", + "link_to": "Pest", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Customer", + "link_to": "Customer", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Chemical", + "link_to": "Chemical", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Tray Type", + "link_to": "Tray Type", + "type": "DocType" + }, + { + "doc_view": "List", + "label": "Resource Consumption Log", + "link_to": "Resource Consumption Log", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Daily Resource Usage Summary", + "link_to": "Daily Resource Usage Summary", + "report_ref_doctype": "Resource Consumption Log", + "type": "Report" } ], "title": "Agri Seedling" diff --git a/agri_seedling/hooks.py b/agri_seedling/hooks.py index 4c974e7..016c655 100644 --- a/agri_seedling/hooks.py +++ b/agri_seedling/hooks.py @@ -132,6 +132,15 @@ has_permission = { # "Field Worker" role; see agri_seedling/utils/auth.py on_session_creation = "agri_seedling.agri_seedling.utils.auth.set_field_worker_session_expiry" + +# Before Request — X-Worker-Token auth hook for mobile field-worker app +# --------------------------------------------------------------------------- +# Validates the X-Worker-Token header on every request and upgrades the session +# from Guest to the proxy user so whitelisted API methods can execute. +before_request = [ + "agri_seedling.agri_seedling.auth_hooks.authenticate_worker_token", +] + # Website Route Rules (kept minimal — mobile app is the field-worker client) # --------------------------------------------------------------------------- website_route_rules = [] diff --git a/agri_seedling/requirements.txt b/agri_seedling/requirements.txt new file mode 100644 index 0000000..aa2e807 --- /dev/null +++ b/agri_seedling/requirements.txt @@ -0,0 +1 @@ +qrcode[pil] diff --git a/requirements.txt b/requirements.txt index f47ffaf..b15c5c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ frappe +pyjwt