diff --git a/church/church_operations/doctype/church_location/church_location.js b/church/church_foundations/doctype/church/church.js
similarity index 76%
rename from church/church_operations/doctype/church_location/church_location.js
rename to church/church_foundations/doctype/church/church.js
index c7589a4..dd0d822 100644
--- a/church/church_operations/doctype/church_location/church_location.js
+++ b/church/church_foundations/doctype/church/church.js
@@ -1,7 +1,7 @@
// Copyright (c) 2026, meichthys and contributors
// For license information, please see license.txt
-// frappe.ui.form.on("Church Location", {
+// frappe.ui.form.on("Church", {
// refresh(frm) {
// },
diff --git a/church/church_foundations/doctype/church/church.json b/church/church_foundations/doctype/church/church.json
index a4a1ef1..165b083 100644
--- a/church/church_foundations/doctype/church/church.json
+++ b/church/church_foundations/doctype/church/church.json
@@ -4,17 +4,17 @@
"allow_rename": 1,
"autoname": "field:church_name",
"creation": "2025-09-17 21:10:06.782386",
- "default_view": "Tree",
+ "default_view": "List",
"description": "A church or church branch.",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"church_name",
"legal_name",
- "founding_date",
- "column_break_rmsm",
"is_group",
"parent_church",
+ "column_break_rmsm",
+ "founding_date",
"address",
"default_bible_translation",
"mission_statement",
@@ -32,7 +32,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Name",
- "reqd": 1
+ "reqd": 1,
+ "unique": 1
},
{
"fieldname": "legal_name",
@@ -60,6 +61,7 @@
"fieldname": "parent_church",
"fieldtype": "Link",
"ignore_user_permissions": 1,
+ "in_list_view": 1,
"label": "Parent Church",
"options": "Church"
},
@@ -123,7 +125,7 @@
"index_web_pages_for_search": 1,
"is_tree": 1,
"links": [],
- "modified": "2025-11-15 22:53:46.638825",
+ "modified": "2026-03-07 17:02:03.356563",
"modified_by": "Administrator",
"module": "Church Foundations",
"name": "Church",
diff --git a/church/church_operations/doctype/church_location/church_location.json b/church/church_operations/doctype/church_location/church_location.json
deleted file mode 100644
index 1da6b37..0000000
--- a/church/church_operations/doctype/church_location/church_location.json
+++ /dev/null
@@ -1,149 +0,0 @@
-{
- "actions": [],
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "format:{title}",
- "creation": "2026-01-05 23:28:09.404374",
- "default_view": "Tree",
- "description": "A physical location associated with the Church. (i.e. Office, Library, etc)",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "church",
- "title",
- "notes",
- "column_break_djot",
- "parent_church_location",
- "is_group",
- "photo",
- "hidden_fields_section",
- "lft",
- "rgt",
- "old_parent"
- ],
- "fields": [
- {
- "fieldname": "church",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Church",
- "options": "Church",
- "search_index": 1
- },
- {
- "allow_in_quick_entry": 1,
- "fieldname": "title",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_preview": 1,
- "in_standard_filter": 1,
- "label": "Title",
- "reqd": 1
- },
- {
- "allow_in_quick_entry": 1,
- "fieldname": "notes",
- "fieldtype": "Text Editor",
- "in_list_view": 1,
- "in_preview": 1,
- "label": "Notes"
- },
- {
- "fieldname": "lft",
- "fieldtype": "Int",
- "hidden": 1,
- "label": "Left",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "rgt",
- "fieldtype": "Int",
- "hidden": 1,
- "label": "Right",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "default": "0",
- "description": "Check this if other locations are located within this location.",
- "fieldname": "is_group",
- "fieldtype": "Check",
- "label": "Is Group"
- },
- {
- "fieldname": "old_parent",
- "fieldtype": "Link",
- "label": "Old Parent",
- "options": "Church Location"
- },
- {
- "description": "If this location is located within another location, choose the other location here.",
- "fieldname": "parent_church_location",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Parent Location",
- "options": "Church Location"
- },
- {
- "fieldname": "column_break_djot",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "hidden_fields_section",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Hidden Fields"
- },
- {
- "fieldname": "photo",
- "fieldtype": "Attach Image",
- "label": "Photo"
- }
- ],
- "grid_page_length": 50,
- "image_field": "photo",
- "index_web_pages_for_search": 1,
- "is_tree": 1,
- "links": [],
- "modified": "2026-01-05 23:51:38.123146",
- "modified_by": "Administrator",
- "module": "Church Operations",
- "name": "Church Location",
- "naming_rule": "Expression",
- "nsm_parent_field": "parent_church_location",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "import": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Church Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "row_format": "Dynamic",
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
diff --git a/church/church_operations/doctype/church_location/church_location.py b/church/church_operations/doctype/church_location/church_location.py
deleted file mode 100644
index 75a8f52..0000000
--- a/church/church_operations/doctype/church_location/church_location.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026, meichthys and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class ChurchLocation(Document):
- pass
diff --git a/church/church_operations/doctype/church_location/test_church_location.py b/church/church_operations/doctype/church_location/test_church_location.py
deleted file mode 100644
index 36754d4..0000000
--- a/church/church_operations/doctype/church_location/test_church_location.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2026, meichthys and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests.utils import FrappeTestCase
-
-
-class TestChurchLocation(FrappeTestCase):
- pass
diff --git a/church/church_operations/workspace/operations/operations.json b/church/church_operations/workspace/operations/operations.json
index 44c7740..840f237 100644
--- a/church/church_operations/workspace/operations/operations.json
+++ b/church/church_operations/workspace/operations/operations.json
@@ -17,7 +17,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Operations Documents",
- "link_count": 3,
+ "link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
@@ -32,16 +32,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Church Locations",
- "link_count": 0,
- "link_to": "Church Location",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
diff --git a/church/church_people/dashboard_chart/members/members.json b/church/church_people/dashboard_chart/members/members.json
new file mode 100644
index 0000000..ebcdab0
--- /dev/null
+++ b/church/church_people/dashboard_chart/members/members.json
@@ -0,0 +1,34 @@
+{
+ "based_on": "membership_date",
+ "chart_name": "Members",
+ "chart_type": "Sum",
+ "creation": "2026-03-07 15:58:56.308560",
+ "currency": "USD",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Person",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Person\",\"is_member\",\"=\",1,false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2026-03-07 16:00:38.065515",
+ "modified": "2026-03-07 16:01:28.327322",
+ "modified_by": "Administrator",
+ "module": "Church People",
+ "name": "Members",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "roles": [],
+ "show_values_over_chart": 1,
+ "source": "",
+ "time_interval": "Monthly",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/church/church_people/dashboard_chart/people/people.json b/church/church_people/dashboard_chart/people/people.json
new file mode 100644
index 0000000..04b21d8
--- /dev/null
+++ b/church/church_people/dashboard_chart/people/people.json
@@ -0,0 +1,33 @@
+{
+ "based_on": "creation",
+ "chart_name": "People",
+ "chart_type": "Sum",
+ "creation": "2026-03-07 15:56:57.922304",
+ "currency": "USD",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Person",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2026-03-07 15:56:57.922304",
+ "modified_by": "Administrator",
+ "module": "Church People",
+ "name": "People",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "parent_document_type": "",
+ "roles": [],
+ "show_values_over_chart": 1,
+ "source": "",
+ "time_interval": "Weekly",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/church/church_people/doctype/family_members/family_members.json b/church/church_people/doctype/family_members/family_members.json
index bbb35b7..5673137 100644
--- a/church/church_people/doctype/family_members/family_members.json
+++ b/church/church_people/doctype/family_members/family_members.json
@@ -8,7 +8,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "member"
+ "member",
+ "relationship_to_head"
],
"fields": [
{
@@ -19,6 +20,13 @@
"label": "Family Member",
"options": "Person",
"reqd": 1
+ },
+ {
+ "fieldname": "relationship_to_head",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Relationship to Head",
+ "options": "Person Relation Type"
}
],
"grid_page_length": 50,
diff --git a/church/church_people/doctype/person/person.json b/church/church_people/doctype/person/person.json
index f736001..00efced 100644
--- a/church/church_people/doctype/person/person.json
+++ b/church/church_people/doctype/person/person.json
@@ -47,7 +47,6 @@
"anniversary",
"spouse",
"column_break_fkwj",
- "relationships",
"notes_tab",
"notes"
],
@@ -278,15 +277,6 @@
"label": "Notes",
"print_hide": 1
},
- {
- "allow_in_quick_entry": 1,
- "description": "Note: Relations are not automatically reciprocal. (i.e. If you are editing 'Mark' and add 'Jane' as 'Sister', Jane's record will not automatically be updated to show 'Mark' as 'Brother'. ",
- "fieldname": "relationships",
- "fieldtype": "Table",
- "label": "Notable Relationships",
- "options": "Person Relation",
- "print_hide": 1
- },
{
"allow_in_quick_entry": 1,
"default": "Unknown",
diff --git a/church/church_people/doctype/person/person.py b/church/church_people/doctype/person/person.py
index e6274bd..71fc11b 100644
--- a/church/church_people/doctype/person/person.py
+++ b/church/church_people/doctype/person/person.py
@@ -113,9 +113,7 @@ class Person(Document):
@frappe.whitelist()
def new_family_from_person(self):
# Check if a family with this person's name already exists
- existing_family = frappe.db.exists(
- "Family", {"family_name": f"{self.last_name} - {self.first_name}"}
- )
+ existing_family = frappe.db.exists("Family", {"family_name": f"{self.last_name} - {self.first_name}"})
if existing_family:
# Set this person's family to the existing one
diff --git a/church/church_people/number_card/church_members/church_members.json b/church/church_people/number_card/church_members/church_members.json
index 3218648..521a772 100644
--- a/church/church_people/number_card/church_members/church_members.json
+++ b/church/church_people/number_card/church_members/church_members.json
@@ -11,8 +11,8 @@
"idx": 0,
"is_public": 0,
"is_standard": 1,
- "label": "Church Member Count",
- "modified": "2025-09-21 22:43:41.991451",
+ "label": "Member Count",
+ "modified": "2026-03-07 15:50:10.752238",
"modified_by": "Administrator",
"module": "Church People",
"name": "Church Members",
diff --git a/church/church_operations/doctype/church_location/__init__.py b/church/church_people/report/church_directory_report/__init__.py
similarity index 100%
rename from church/church_operations/doctype/church_location/__init__.py
rename to church/church_people/report/church_directory_report/__init__.py
diff --git a/church/church_people/report/church_directory_report/church_directory.html b/church/church_people/report/church_directory_report/church_directory.html
new file mode 100644
index 0000000..744c0b7
--- /dev/null
+++ b/church/church_people/report/church_directory_report/church_directory.html
@@ -0,0 +1,525 @@
+
+
+
+
+{{ church.church_name }} – Church Directory
+
+
+
+
+
+
+
{{ church.church_name }}
+
+ {% if church.legal_name and church.legal_name != church.church_name %}
+
{{ church.legal_name }}
+ {% endif %}
+
+ {% if church_address %}
+
+ {{ church_address.address_line1 }}{% if church_address.address_line2 %}
{{ church_address.address_line2 }}{% endif %}
+ {{ church_address.city }}{% if church_address.state %}, {{ church_address.state }}{% endif %}{% if church_address.pincode %} {{ church_address.pincode }}{% endif %}
+
+ {% endif %}
+
+
Church Directory
+
{{ generated_date }}
+
+ {% if church.mission_statement %}
+
{{ church.mission_statement }}
+ {% endif %}
+
+
+
+{% set ns = namespace(current_letter='') %}
+{% for entry in all_entries %}
+ {% set first_letter = (entry.sort_name[0] if entry.sort_name else '#').upper() %}
+ {% if first_letter != ns.current_letter %}
+ {% set ns.current_letter = first_letter %}
+ {{ first_letter }}
+ {% endif %}
+
+
+
+
+
+
+
+ {% if show_photos %} | {% endif %}
+ Name |
+ {% if show_membership %}Membership | {% endif %}
+ Phone |
+ Email |
+
+
+
+ {% for member in entry.members %}
+
+ {% if show_photos %}
+
+ {% if member.photo %}
+
+ {% endif %}
+ |
+ {% endif %}
+
+ {{ member.full_name }}
+ {% if show_hoh and member.is_head_of_household %}★{% endif %}
+ {% if show_roles and member.positions %}
+ {{ member.positions | join(', ') }}
+ {% endif %}
+ |
+ {% if show_membership %}
+
+ {% if member.membership_status %}
+ {{ member.membership_status }}
+ {% endif %}
+ |
+ {% endif %}
+ {{ member.primary_phone or '' }} |
+ {{ member.email or '' }} |
+
+ {% endfor %}
+
+
+
+{% endfor %}
+
+
+{% if show_birthdays and birthdays %}
+
+
Birthdays
+ {% set ns2 = namespace(current_month='') %}
+ {% for person in birthdays %}
+ {% if person.month_name != ns2.current_month %}
+ {% set ns2.current_month = person.month_name %}
+
{{ person.month_name }}
+ {% endif %}
+
+ {{ person.full_name }}
+ {{ person.month_day }}
+
+ {% endfor %}
+
+{% endif %}
+
+
+{% if show_anniversaries and anniversaries %}
+
+
Anniversaries
+ {% set ns3 = namespace(current_month='') %}
+ {% for couple in anniversaries %}
+ {% if couple.month_name != ns3.current_month %}
+ {% set ns3.current_month = couple.month_name %}
+
{{ couple.month_name }}
+ {% endif %}
+
+ {{ couple.display_name }}
+ {{ couple.month_day }}
+
+ {% endfor %}
+
+{% endif %}
+
+
+{% if show_missionaries and missionaries %}
+
+
Missionaries
+ {% for m in missionaries %}
+
+
+ {% if not m.sensitive and (m.email or m.website) %}
+
+ {% if m.email %}Email: {{ m.email }}{% endif %}
+ {% if m.email and m.website %}
{% endif %}
+ {% if m.website %}Website: {{ m.website }}{% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+{% endif %}
+
+
+
diff --git a/church/church_people/report/church_directory_report/church_directory_report.js b/church/church_people/report/church_directory_report/church_directory_report.js
new file mode 100644
index 0000000..71e5894
--- /dev/null
+++ b/church/church_people/report/church_directory_report/church_directory_report.js
@@ -0,0 +1,115 @@
+frappe.query_reports["Church Directory Report"] = {
+ filters: [
+ {
+ fieldname: "church",
+ label: __("Church"),
+ fieldtype: "Link",
+ options: "Church",
+ reqd: 1,
+ },
+ {
+ fieldname: "include_sub_churches",
+ label: __("Include Sub-Churches"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "members_only",
+ label: __("Members Only"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "show_photos",
+ label: __("Show Photos"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "show_roles",
+ label: __("Show Positions"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "show_membership",
+ label: __("Show Membership Status"),
+ fieldtype: "Check",
+ default: 1,
+ },
+ {
+ fieldname: "show_hoh",
+ label: __("Show Head of Household"),
+ fieldtype: "Check",
+ default: 1,
+ },
+ {
+ fieldname: "show_birthdays",
+ label: __("Include Birthday List"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "show_anniversaries",
+ label: __("Include Anniversary List"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ {
+ fieldname: "show_missionaries",
+ label: __("Include Missionaries"),
+ fieldtype: "Check",
+ default: 0,
+ },
+ ],
+
+ onload: function (report) {
+ report.page.add_inner_button(__('Print Directory'), function () {
+ const church = report.get_filter_value('church');
+ const include_sub_churches = report.get_filter_value('include_sub_churches') ? 1 : 0;
+ const members_only = report.get_filter_value('members_only') ? 1 : 0;
+ const show_photos = report.get_filter_value('show_photos') ? 1 : 0;
+ const show_roles = report.get_filter_value('show_roles') ? 1 : 0;
+ const show_membership = report.get_filter_value('show_membership') ? 1 : 0;
+ const show_hoh = report.get_filter_value('show_hoh') ? 1 : 0;
+ const show_birthdays = report.get_filter_value('show_birthdays') ? 1 : 0;
+ const show_anniversaries = report.get_filter_value('show_anniversaries') ? 1 : 0;
+ const show_missionaries = report.get_filter_value('show_missionaries') ? 1 : 0;
+
+ if (!church) {
+ frappe.msgprint(__('Please select a Church first.'));
+ return;
+ }
+
+ frappe.call({
+ method: 'church.church_people.report.church_directory_report.church_directory_report.get_directory_html',
+ args: {
+ church,
+ include_sub_churches,
+ members_only,
+ show_photos,
+ show_roles,
+ show_membership,
+ show_hoh,
+ show_birthdays,
+ show_anniversaries,
+ show_missionaries,
+ },
+ freeze: true,
+ freeze_message: __('Generating Church Directory\u2026'),
+ callback: function (r) {
+ if (!r.message) return;
+ const win = window.open('', '_blank');
+ win.document.open();
+ win.document.write(r.message);
+ win.document.close();
+ // Allow images/fonts to load before triggering print
+ win.addEventListener('load', function () {
+ win.focus();
+ win.print();
+ });
+ },
+ });
+ });
+ },
+};
diff --git a/church/church_people/report/church_directory_report/church_directory_report.json b/church/church_people/report/church_directory_report/church_directory_report.json
new file mode 100644
index 0000000..f08fa68
--- /dev/null
+++ b/church/church_people/report/church_directory_report/church_directory_report.json
@@ -0,0 +1,35 @@
+{
+ "add_total_row": 0,
+ "add_translate_data": 0,
+ "columns": [],
+ "creation": "2026-03-08 00:00:00.000000",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letterhead": null,
+ "modified": "2026-03-08 00:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "Church People",
+ "name": "Church Directory Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Family",
+ "report_name": "Church Directory Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Church Manager"
+ },
+ {
+ "role": "Church User"
+ }
+ ],
+ "timeout": 0
+}
diff --git a/church/church_people/report/church_directory_report/church_directory_report.py b/church/church_people/report/church_directory_report/church_directory_report.py
new file mode 100644
index 0000000..6200962
--- /dev/null
+++ b/church/church_people/report/church_directory_report/church_directory_report.py
@@ -0,0 +1,395 @@
+import calendar
+import os
+
+import frappe
+from frappe.utils import today as frappe_today
+
+
+def execute(filters=None):
+ return get_columns(), get_data(filters)
+
+
+def get_columns():
+ return [
+ {"fieldname": "family", "fieldtype": "Link", "label": "Family", "options": "Family", "width": 200},
+ {
+ "fieldname": "head_of_household",
+ "fieldtype": "Link",
+ "label": "Head of Household",
+ "options": "Person",
+ "width": 180,
+ },
+ {"fieldname": "church", "fieldtype": "Link", "label": "Church", "options": "Church", "width": 160},
+ {"fieldname": "city", "fieldtype": "Data", "label": "City", "width": 140},
+ {"fieldname": "state", "fieldtype": "Data", "label": "State", "width": 100},
+ {"fieldname": "member_count", "fieldtype": "Int", "label": "Members", "width": 80},
+ ]
+
+
+def get_data(filters):
+ if not filters or not filters.get("church"):
+ return []
+
+ church = filters.get("church")
+ members_only = frappe.utils.cint(filters.get("members_only", 0))
+ include_sub_churches = frappe.utils.cint(filters.get("include_sub_churches", 0))
+
+ churches = get_church_scope(church, include_sub_churches)
+ church_in = build_in_clause(churches)
+
+ families = frappe.db.sql(
+ f"""
+ SELECT
+ f.name AS family_id,
+ f.family_name,
+ f.church,
+ COALESCE(a.city, '') AS city,
+ COALESCE(a.state, '') AS state
+ FROM `tabFamily` f
+ LEFT JOIN `tabAddress` a ON a.name = f.home_address
+ WHERE f.church IN {church_in}
+ ORDER BY f.family_name ASC
+ """,
+ as_dict=True,
+ )
+
+ member_filter = "AND p.is_member = 1" if members_only else ""
+
+ all_members = frappe.db.sql(
+ f"""
+ SELECT p.name, p.full_name, p.family, p.is_head_of_household
+ FROM `tabPerson` p
+ WHERE p.church IN {church_in}
+ AND p.family IS NOT NULL AND p.family != ''
+ {member_filter}
+ """,
+ as_dict=True,
+ )
+
+ members_by_family = {}
+ for m in all_members:
+ members_by_family.setdefault(m.family, []).append(m)
+
+ result = []
+ for family in families:
+ members = members_by_family.get(family.family_id, [])
+ head = next((m.name for m in members if m.is_head_of_household), None)
+ if not head and members:
+ head = members[0].name
+ result.append(
+ {
+ "family": family.family_id,
+ "head_of_household": head or "",
+ "church": family.church,
+ "city": family.city,
+ "state": family.state,
+ "member_count": len(members),
+ }
+ )
+
+ return result
+
+
+@frappe.whitelist()
+def get_directory_html(
+ church,
+ members_only=0,
+ include_sub_churches=0,
+ show_photos=0,
+ show_roles=0,
+ show_membership=1,
+ show_hoh=1,
+ show_birthdays=0,
+ show_anniversaries=0,
+ show_missionaries=0,
+):
+ """Generate the full HTML for the church directory, ready to print."""
+ members_only = frappe.utils.cint(members_only)
+ include_sub_churches = frappe.utils.cint(include_sub_churches)
+ show_photos = frappe.utils.cint(show_photos)
+ show_roles = frappe.utils.cint(show_roles)
+ show_membership = frappe.utils.cint(show_membership)
+ show_hoh = frappe.utils.cint(show_hoh)
+ show_birthdays = frappe.utils.cint(show_birthdays)
+ show_anniversaries = frappe.utils.cint(show_anniversaries)
+ show_missionaries = frappe.utils.cint(show_missionaries)
+
+ church_doc = frappe.get_doc("Church", church)
+ church_address = None
+ if church_doc.address:
+ church_address = frappe.get_doc("Address", church_doc.address)
+
+ churches = get_church_scope(church, include_sub_churches)
+ church_in = build_in_clause(churches)
+
+ families = frappe.db.sql(
+ f"""
+ SELECT
+ f.name AS family_id,
+ f.family_name,
+ f.church AS church_name,
+ f.photo AS family_photo,
+ COALESCE(a.address_line1, '') AS address_line1,
+ COALESCE(a.address_line2, '') AS address_line2,
+ COALESCE(a.city, '') AS city,
+ COALESCE(a.state, '') AS state,
+ COALESCE(a.pincode, '') AS pincode
+ FROM `tabFamily` f
+ LEFT JOIN `tabAddress` a ON a.name = f.home_address
+ WHERE f.church IN {church_in}
+ ORDER BY f.family_name ASC
+ """,
+ as_dict=True,
+ )
+
+ member_filter = "AND p.is_member = 1" if members_only else ""
+
+ all_members = frappe.db.sql(
+ f"""
+ SELECT
+ p.name AS person_name,
+ p.full_name,
+ p.primary_phone,
+ p.email,
+ p.membership_status,
+ p.is_head_of_household,
+ p.photo,
+ p.family
+ FROM `tabPerson` p
+ WHERE p.church IN {church_in}
+ AND p.family IS NOT NULL AND p.family != ''
+ {member_filter}
+ ORDER BY p.family, p.is_head_of_household DESC, p.last_name, p.first_name
+ """,
+ as_dict=True,
+ )
+
+ # Fetch active positions if requested
+ roles_by_person = {}
+ if show_roles:
+ today = frappe_today()
+ active_positions = frappe.db.sql(
+ f"""
+ SELECT pos.parent AS person_name, pos.position
+ FROM `tabPosition` pos
+ INNER JOIN `tabPerson` p ON p.name = pos.parent
+ WHERE pos.parenttype = 'Person'
+ AND pos.position IS NOT NULL
+ AND pos.start_date <= %(today)s
+ AND (pos.end_date IS NULL OR pos.end_date >= %(today)s)
+ AND p.church IN {church_in}
+ ORDER BY pos.parent, pos.start_date
+ """,
+ {"today": today},
+ as_dict=True,
+ )
+ for row in active_positions:
+ roles_by_person.setdefault(row.person_name, []).append(row.position)
+
+ for m in all_members:
+ m["positions"] = roles_by_person.get(m.person_name, [])
+
+ members_by_family = {}
+ for m in all_members:
+ members_by_family.setdefault(m.family, []).append(m)
+
+ individuals_raw = frappe.db.sql(
+ f"""
+ SELECT
+ p.name AS person_name,
+ p.full_name,
+ p.last_name,
+ p.primary_phone,
+ p.email,
+ p.membership_status,
+ p.photo,
+ p.church AS church_name
+ FROM `tabPerson` p
+ WHERE p.church IN {church_in}
+ AND (p.family IS NULL OR p.family = '')
+ {member_filter}
+ ORDER BY p.last_name, p.first_name
+ """,
+ as_dict=True,
+ )
+
+ for p in individuals_raw:
+ p["positions"] = roles_by_person.get(p.person_name, [])
+ p["is_head_of_household"] = 0
+
+ # Build merged sorted entry list
+ all_entries = []
+
+ for family in families:
+ members = members_by_family.get(family.family_id, [])
+ if members:
+ all_entries.append(
+ {
+ "sort_name": family.family_name,
+ "display_name": family.family_name + " Family",
+ "is_individual": False,
+ "church_name": family.church_name,
+ "family_photo": family.family_photo,
+ "address_line1": family.address_line1,
+ "address_line2": family.address_line2,
+ "city": family.city,
+ "state": family.state,
+ "pincode": family.pincode,
+ "members": members,
+ }
+ )
+
+ for person in individuals_raw:
+ sort_key = (person.get("last_name") or person.get("full_name") or "").strip()
+ all_entries.append(
+ {
+ "sort_name": sort_key,
+ "display_name": person.full_name,
+ "is_individual": True,
+ "church_name": person.church_name,
+ "family_photo": None,
+ "address_line1": "",
+ "address_line2": "",
+ "city": "",
+ "state": "",
+ "pincode": "",
+ "members": [person],
+ }
+ )
+
+ all_entries.sort(key=lambda e: (e["sort_name"] or "").upper())
+
+ # ── Birthdays ────────────────────────────────────────────────
+ birthdays = []
+ if show_birthdays:
+ raw_birthdays = frappe.db.sql(
+ f"""
+ SELECT
+ p.full_name,
+ p.birthday,
+ MONTH(p.birthday) AS birth_month,
+ DAY(p.birthday) AS birth_day
+ FROM `tabPerson` p
+ WHERE p.church IN {church_in}
+ AND p.birthday IS NOT NULL
+ {member_filter}
+ ORDER BY MONTH(p.birthday), DAY(p.birthday), p.last_name, p.first_name
+ """,
+ as_dict=True,
+ )
+ for row in raw_birthdays:
+ row["month_name"] = calendar.month_name[int(row.birth_month)]
+ row["month_day"] = f"{calendar.month_name[int(row.birth_month)]} {int(row.birth_day)}"
+ birthdays.append(row)
+
+ # ── Anniversaries ────────────────────────────────────────────
+ anniversaries = []
+ if show_anniversaries:
+ raw_anniversaries = frappe.db.sql(
+ f"""
+ SELECT
+ p.name AS person_name,
+ p.spouse AS spouse_name,
+ p.first_name AS person_first,
+ p.full_name AS person_full,
+ s.first_name AS spouse_first,
+ COALESCE(f.family_name, '') AS family_name,
+ p.anniversary,
+ MONTH(p.anniversary) AS ann_month,
+ DAY(p.anniversary) AS ann_day
+ FROM `tabPerson` p
+ LEFT JOIN `tabPerson` s ON s.name = p.spouse
+ LEFT JOIN `tabFamily` f ON f.name = p.family
+ WHERE p.church IN {church_in}
+ AND p.anniversary IS NOT NULL
+ AND p.is_married = 1
+ {member_filter}
+ ORDER BY MONTH(p.anniversary), DAY(p.anniversary), p.last_name, p.first_name
+ """,
+ as_dict=True,
+ )
+ seen_persons = set()
+ for row in raw_anniversaries:
+ if row.person_name in seen_persons:
+ continue
+ seen_persons.add(row.person_name)
+ if row.spouse_name:
+ seen_persons.add(row.spouse_name)
+ row["month_name"] = calendar.month_name[int(row.ann_month)]
+ row["month_day"] = f"{calendar.month_name[int(row.ann_month)]} {int(row.ann_day)}"
+ if row.spouse_first and row.family_name:
+ row["display_name"] = f"{row.person_first} & {row.spouse_first} {row.family_name}"
+ elif row.spouse_first:
+ row["display_name"] = f"{row.person_full} & {row.spouse_first}"
+ else:
+ row["display_name"] = row.person_full
+ anniversaries.append(row)
+
+ # ── Missionaries ─────────────────────────────────────────────
+ missionaries = []
+ if show_missionaries:
+ missionaries = frappe.db.sql(
+ f"""
+ SELECT
+ m.title,
+ m.agency,
+ m.country,
+ m.email,
+ m.website,
+ m.photo,
+ m.sensitive,
+ m.mission_statement
+ FROM `tabMissionary` m
+ WHERE m.church IN {church_in}
+ ORDER BY m.title
+ """,
+ as_dict=True,
+ )
+
+ template_path = os.path.join(os.path.dirname(__file__), "church_directory.html")
+ with open(template_path) as f:
+ template = f.read()
+
+ context = {
+ "church": church_doc,
+ "church_address": church_address,
+ "all_entries": all_entries,
+ "show_church_label": bool(include_sub_churches),
+ "show_photos": show_photos,
+ "show_roles": show_roles,
+ "show_membership": show_membership,
+ "show_hoh": show_hoh,
+ "birthdays": birthdays,
+ "anniversaries": anniversaries,
+ "missionaries": missionaries,
+ "show_birthdays": show_birthdays,
+ "show_anniversaries": show_anniversaries,
+ "show_missionaries": show_missionaries,
+ "generated_date": frappe.utils.formatdate(frappe.utils.nowdate(), "MMMM yyyy"),
+ }
+
+ return frappe.render_template(template, context)
+
+
+def get_church_scope(church, include_sub_churches):
+ """Return the church itself, or the full subtree if include_sub_churches is set."""
+ if not include_sub_churches:
+ return [church]
+
+ return frappe.db.sql_list(
+ """
+ SELECT child.name
+ FROM `tabChurch` child
+ INNER JOIN `tabChurch` parent
+ ON child.lft >= parent.lft AND child.rgt <= parent.rgt
+ WHERE parent.name = %s
+ ORDER BY child.lft
+ """,
+ church,
+ )
+
+
+def build_in_clause(values):
+ """Return a safely escaped SQL IN clause string, e.g. ('A', 'B')."""
+ escaped = [frappe.db.escape(v) for v in values]
+ return "(" + ", ".join(escaped) + ")"
diff --git a/church/church_people/workspace/people/people.json b/church/church_people/workspace/people/people.json
index 0ec983f..f5b0a41 100644
--- a/church/church_people/workspace/people/people.json
+++ b/church/church_people/workspace/people/people.json
@@ -1,11 +1,11 @@
{
"charts": [
{
- "chart_name": "Persons Count",
+ "chart_name": "People",
"label": "Persons"
},
{
- "chart_name": "Member Count (New by Month)",
+ "chart_name": "Members",
"label": "New Members"
}
],
@@ -82,12 +82,23 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Church Directory",
+ "link_count": 0,
+ "link_to": "Church Directory Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "report_ref_doctype": "Family",
+ "type": "Link"
+ },
{
"description": "These are some pre-prepared reports. You can always create your own reports by going to `[Document] Report`, filtering the report, then choosing `save as` in the `...` menu.",
"hidden": 0,
"is_query_report": 0,
"label": "Person Reports",
- "link_count": 5,
+ "link_count": 6,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
@@ -144,9 +155,19 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Church Directory",
+ "link_count": 0,
+ "link_to": "Church Directory Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2026-03-04 23:13:06.657575",
+ "modified": "2026-03-07 16:11:26.024550",
"modified_by": "Administrator",
"module": "Church People",
"name": "People",
@@ -157,11 +178,11 @@
},
{
"label": "Members",
- "number_card_name": "Church Members"
+ "number_card_name": "Members"
},
{
"label": "Families",
- "number_card_name": "Church Families"
+ "number_card_name": "Families"
}
],
"owner": "Administrator",