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 %} + + {% 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 %} + +
+
+ +
+
{{ entry.display_name }}
+ {% if show_church_label %}
{{ entry.church_name }}
{% endif %} +
+ + +
+ {% if entry.address_line1 or entry.city %} +
+ {% if entry.address_line1 %}{{ entry.address_line1 }}{% if entry.address_line2 %}, {{ entry.address_line2 }}{% endif %}
{% endif %} + {% if entry.city %}{{ entry.city }}{% if entry.state %}, {{ entry.state }}{% endif %}{% if entry.pincode %} {{ entry.pincode }}{% endif %}{% endif %} +
+ {% endif %} + {% if show_photos and entry.family_photo %} + {{ entry.display_name }} + {% endif %} +
+
+ + + + + {% if show_photos %}{% endif %} + + {% if show_membership %}{% endif %} + + + + + + {% for member in entry.members %} + + {% if show_photos %} + + {% endif %} + + {% if show_membership %} + + {% endif %} + + + + {% endfor %} + +
NameMembershipPhone
+ {% if member.photo %} + + {% 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 member.membership_status %} + {{ member.membership_status }} + {% endif %} + {{ member.primary_phone or '' }}
+
+{% 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 show_photos and m.photo %} + {{ m.title }} + {% endif %} +
+
{{ m.title }}
+ {% if m.agency %}
{{ m.agency }}
{% endif %} + {% if m.country and not m.sensitive %}
{{ m.country }}
{% endif %} + {% if m.sensitive %}Sensitive{% endif %} + {% if m.mission_statement %}
{{ m.mission_statement }}
{% endif %} +
+
+ {% 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",