mirror of
https://github.com/meichthys/church.git
synced 2026-04-04 21:19:55 +00:00
Merge branch 'develop' into multi-church
This commit is contained in:
commit
fcbe8b3565
@ -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) {
|
||||
|
||||
// },
|
||||
@ -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",
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
34
church/church_people/dashboard_chart/members/members.json
Normal file
34
church/church_people/dashboard_chart/members/members.json
Normal file
@ -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": []
|
||||
}
|
||||
33
church/church_people/dashboard_chart/people/people.json
Normal file
33
church/church_people/dashboard_chart/people/people.json
Normal file
@ -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": []
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -0,0 +1,525 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ church.church_name }} – Church Directory</title>
|
||||
<style>
|
||||
/* ── Page setup ─────────────────────────────────────────────── */
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 18mm 18mm 18mm 18mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: 11pt;
|
||||
color: #1a1a2e;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ── Cover page ─────────────────────────────────────────────── */
|
||||
.cover {
|
||||
break-after: page;
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
padding: 40mm 20mm;
|
||||
}
|
||||
|
||||
.cover-church-name {
|
||||
font-size: 30pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cover-legal-name {
|
||||
font-size: 14pt;
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cover-address {
|
||||
font-size: 12pt;
|
||||
color: #444;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
font-size: 24pt;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
color: #1f3c88;
|
||||
border-top: 2px solid #1f3c88;
|
||||
border-bottom: 2px solid #1f3c88;
|
||||
padding: 10px 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cover-date {
|
||||
font-size: 13pt;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.cover-mission {
|
||||
font-size: 12pt;
|
||||
font-style: italic;
|
||||
color: #444;
|
||||
max-width: 420px;
|
||||
line-height: 1.6;
|
||||
border-left: 3px solid #1f3c88;
|
||||
padding-left: 14px;
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* ── Directory section header ───────────────────────────────── */
|
||||
.alpha-divider {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
border-bottom: 2px solid #1f3c88;
|
||||
margin: 14px 0 8px 0;
|
||||
padding-bottom: 2px;
|
||||
break-after: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
/* ── Family block ───────────────────────────────────────────── */
|
||||
.family-block {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #d0d8e8;
|
||||
border-left: 4px solid #1f3c88;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #f9fafd;
|
||||
}
|
||||
|
||||
.family-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.family-name {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
}
|
||||
|
||||
.family-church-label {
|
||||
font-size: 8.5pt;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Right side of family header: address + optional photo */
|
||||
.family-meta {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.family-address {
|
||||
font-size: 9.5pt;
|
||||
color: #555;
|
||||
text-align: right;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.family-photo-img {
|
||||
height: 150px;
|
||||
width: auto;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #d0d8e8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Member table ───────────────────────────────────────────── */
|
||||
.member-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 4px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.member-table thead tr {
|
||||
border-bottom: 1px solid #c5cfe0;
|
||||
}
|
||||
|
||||
.member-table th {
|
||||
font-size: 8.5pt;
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
padding: 2px 6px 3px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.member-table td {
|
||||
padding: 3px 6px 3px 0;
|
||||
vertical-align: middle;
|
||||
color: #222;
|
||||
border-bottom: 1px dotted #e4e8f0;
|
||||
}
|
||||
|
||||
.member-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.col-photo { width: 58px; padding-right: 4px !important; }
|
||||
.col-name { width: 30%; font-weight: 600; }
|
||||
.col-membership { width: 18%; }
|
||||
.col-phone { width: 22%; }
|
||||
.col-email { width: 26%; word-break: break-all; }
|
||||
|
||||
.member-photo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hoh-star {
|
||||
color: #1f3c88;
|
||||
font-size: 9pt;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
font-size: 8.5pt;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
background: #e8edf8;
|
||||
color: #1f3c88;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-roles {
|
||||
font-size: 8pt;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* ── Appendix sections (birthdays, anniversaries, missionaries) */
|
||||
.appendix-section {
|
||||
break-before: page;
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.appendix-heading {
|
||||
font-size: 20pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
border-bottom: 2px solid #1f3c88;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.month-divider {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
margin: 14px 0 4px 0;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid #d0d8e8;
|
||||
break-after: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.calendar-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px dotted #e4e8f0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.calendar-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calendar-name {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.calendar-date {
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Missionary cards ───────────────────────────────────────── */
|
||||
.missionary-block {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #d0d8e8;
|
||||
border-left: 4px solid #1f3c88;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
background: #f9fafd;
|
||||
}
|
||||
|
||||
.missionary-header {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.missionary-photo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #d0d8e8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.missionary-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.missionary-title {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
color: #1f3c88;
|
||||
}
|
||||
|
||||
.missionary-meta {
|
||||
font-size: 10pt;
|
||||
color: #555;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.missionary-statement {
|
||||
font-size: 9.5pt;
|
||||
color: #444;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.missionary-contact {
|
||||
font-size: 9.5pt;
|
||||
color: #444;
|
||||
margin-top: 6px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sensitive-badge {
|
||||
display: inline-block;
|
||||
font-size: 8pt;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────────── */
|
||||
@page {
|
||||
@bottom-center {
|
||||
content: "{{ church.church_name }} Church Directory – {{ generated_date }}";
|
||||
font-family: Georgia, serif;
|
||||
font-size: 8pt;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
COVER PAGE
|
||||
═════════════════════════════════════════════════════════════ -->
|
||||
<div class="cover">
|
||||
<div class="cover-church-name">{{ church.church_name }}</div>
|
||||
|
||||
{% if church.legal_name and church.legal_name != church.church_name %}
|
||||
<div class="cover-legal-name">{{ church.legal_name }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if church_address %}
|
||||
<div class="cover-address">
|
||||
{{ church_address.address_line1 }}{% if church_address.address_line2 %}<br>{{ church_address.address_line2 }}{% endif %}<br>
|
||||
{{ church_address.city }}{% if church_address.state %}, {{ church_address.state }}{% endif %}{% if church_address.pincode %} {{ church_address.pincode }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="cover-title">Church Directory</div>
|
||||
<div class="cover-date">{{ generated_date }}</div>
|
||||
|
||||
{% if church.mission_statement %}
|
||||
<div class="cover-mission">{{ church.mission_statement }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
DIRECTORY (families + individuals merged alphabetically)
|
||||
═════════════════════════════════════════════════════════════ -->
|
||||
{% 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 %}
|
||||
<div class="alpha-divider">{{ first_letter }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="family-block">
|
||||
<div class="family-header">
|
||||
<!-- Left: name + optional sub-church label -->
|
||||
<div>
|
||||
<div class="family-name">{{ entry.display_name }}</div>
|
||||
{% if show_church_label %}<div class="family-church-label">{{ entry.church_name }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right: address + optional family photo -->
|
||||
<div class="family-meta">
|
||||
{% if entry.address_line1 or entry.city %}
|
||||
<div class="family-address">
|
||||
{% if entry.address_line1 %}{{ entry.address_line1 }}{% if entry.address_line2 %}, {{ entry.address_line2 }}{% endif %}<br>{% endif %}
|
||||
{% if entry.city %}{{ entry.city }}{% if entry.state %}, {{ entry.state }}{% endif %}{% if entry.pincode %} {{ entry.pincode }}{% endif %}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_photos and entry.family_photo %}
|
||||
<img class="family-photo-img" src="{{ entry.family_photo }}" alt="{{ entry.display_name }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if show_photos %}<th class="col-photo"></th>{% endif %}
|
||||
<th class="col-name">Name</th>
|
||||
{% if show_membership %}<th class="col-membership">Membership</th>{% endif %}
|
||||
<th class="col-phone">Phone</th>
|
||||
<th class="col-email">Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in entry.members %}
|
||||
<tr>
|
||||
{% if show_photos %}
|
||||
<td class="col-photo">
|
||||
{% if member.photo %}
|
||||
<img class="member-photo" src="{{ member.photo }}" alt="">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="col-name">
|
||||
{{ member.full_name }}
|
||||
{% if show_hoh and member.is_head_of_household %}<span class="hoh-star" title="Head of Household">★</span>{% endif %}
|
||||
{% if show_roles and member.positions %}
|
||||
<div class="member-roles">{{ member.positions | join(', ') }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if show_membership %}
|
||||
<td class="col-membership">
|
||||
{% if member.membership_status %}
|
||||
<span class="status-badge">{{ member.membership_status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="col-phone">{{ member.primary_phone or '' }}</td>
|
||||
<td class="col-email">{{ member.email or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
BIRTHDAYS
|
||||
═════════════════════════════════════════════════════════════ -->
|
||||
{% if show_birthdays and birthdays %}
|
||||
<div class="appendix-section">
|
||||
<div class="appendix-heading">Birthdays</div>
|
||||
{% set ns2 = namespace(current_month='') %}
|
||||
{% for person in birthdays %}
|
||||
{% if person.month_name != ns2.current_month %}
|
||||
{% set ns2.current_month = person.month_name %}
|
||||
<div class="month-divider">{{ person.month_name }}</div>
|
||||
{% endif %}
|
||||
<div class="calendar-row">
|
||||
<span class="calendar-name">{{ person.full_name }}</span>
|
||||
<span class="calendar-date">{{ person.month_day }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
ANNIVERSARIES
|
||||
═════════════════════════════════════════════════════════════ -->
|
||||
{% if show_anniversaries and anniversaries %}
|
||||
<div class="appendix-section">
|
||||
<div class="appendix-heading">Anniversaries</div>
|
||||
{% set ns3 = namespace(current_month='') %}
|
||||
{% for couple in anniversaries %}
|
||||
{% if couple.month_name != ns3.current_month %}
|
||||
{% set ns3.current_month = couple.month_name %}
|
||||
<div class="month-divider">{{ couple.month_name }}</div>
|
||||
{% endif %}
|
||||
<div class="calendar-row">
|
||||
<span class="calendar-name">{{ couple.display_name }}</span>
|
||||
<span class="calendar-date">{{ couple.month_day }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
MISSIONARIES
|
||||
═════════════════════════════════════════════════════════════ -->
|
||||
{% if show_missionaries and missionaries %}
|
||||
<div class="appendix-section">
|
||||
<div class="appendix-heading">Missionaries</div>
|
||||
{% for m in missionaries %}
|
||||
<div class="missionary-block">
|
||||
<div class="missionary-header">
|
||||
{% if show_photos and m.photo %}
|
||||
<img class="missionary-photo" src="{{ m.photo }}" alt="{{ m.title }}">
|
||||
{% endif %}
|
||||
<div class="missionary-info">
|
||||
<div class="missionary-title">{{ m.title }}</div>
|
||||
{% if m.agency %}<div class="missionary-meta">{{ m.agency }}</div>{% endif %}
|
||||
{% if m.country and not m.sensitive %}<div class="missionary-meta">{{ m.country }}</div>{% endif %}
|
||||
{% if m.sensitive %}<span class="sensitive-badge">Sensitive</span>{% endif %}
|
||||
{% if m.mission_statement %}<div class="missionary-statement">{{ m.mission_statement }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if not m.sensitive and (m.email or m.website) %}
|
||||
<div class="missionary-contact">
|
||||
{% if m.email %}Email: {{ m.email }}{% endif %}
|
||||
{% if m.email and m.website %}<br>{% endif %}
|
||||
{% if m.website %}Website: {{ m.website }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -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();
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
@ -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) + ")"
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user