feat(Accounting): Process Statement Of Accounts (#22901)
* feat: Process Statement Of Accounts initial commit
* fix: add jinja supported inputs for subject and body in email settings
* feat: utils support in template, tested autoemail, fixed issues
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
new file mode 100644
index 0000000..e1ddeff
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -0,0 +1,89 @@
+<h1 class="text-center" style="page-break-before:always">{{ filters.party[0] }}</h1>
+<h3 class="text-center">{{ _("Statement of Accounts") }}</h3>
+
+<h5 class="text-center">
+ {{ frappe.format(filters.from_date, 'Date')}}
+ {{ _("to") }}
+ {{ frappe.format(filters.to_date, 'Date')}}
+</h5>
+
+<table class="table table-bordered">
+ <thead>
+ <tr>
+ <th style="width: 12%">{{ _("Date") }}</th>
+ <th style="width: 15%">{{ _("Ref") }}</th>
+ <th style="width: 25%">{{ _("Party") }}</th>
+ <th style="width: 15%">{{ _("Debit") }}</th>
+ <th style="width: 15%">{{ _("Credit") }}</th>
+ <th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for row in data %}
+ <tr>
+ {% if(row.posting_date) %}
+ <td>{{ frappe.format(row.posting_date, 'Date') }}</td>
+ <td>{{ row.voucher_type }}
+ <br>{{ row.voucher_no }}</td>
+ <td>
+ {% if not (filters.party or filters.account) %}
+ {{ row.party or row.account }}
+ <br>
+ {% endif %}
+
+ {{ _("Against") }}: {{ row.against }}
+ <br>{{ _("Remarks") }}: {{ row.remarks }}
+ {% if row.bill_no %}
+ <br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
+ {% endif %}
+ </td>
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td>
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}</td>
+ {% else %}
+ <td></td>
+ <td></td>
+ <td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
+ <td style="text-align: right">
+ {{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}
+ </td>
+ <td style="text-align: right">
+ {{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}
+ </td>
+ {% endif %}
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+<br><br>
+{% if aging %}
+<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}</h3>
+<h5 class="text-center">
+ {{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
+</h5>
+<br>
+
+<table class="table table-bordered">
+ <thead>
+ <tr>
+ <th style="width: 12%">30 Days</th>
+ <th style="width: 15%">60 Days</th>
+ <th style="width: 25%">90 Days</th>
+ <th style="width: 15%">120 Days</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{{ aging.range1 }}</td>
+ <td>{{ aging.range2 }}</td>
+ <td>{{ aging.range3 }}</td>
+ <td>{{ aging.range4 }}</td>
+ </tr>
+ </tbody>
+</table>
+{% endif %}
+<p class="text-right text-muted">Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}</p>
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
new file mode 100644
index 0000000..7425132
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -0,0 +1,132 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Process Statement Of Accounts', {
+ view_properties: function(frm) {
+ frappe.route_options = {doc_type: 'Customer'};
+ frappe.set_route("Form", "Customize Form");
+ },
+ refresh: function(frm){
+ if(!frm.doc.__islocal) {
+ frm.add_custom_button('Send Emails',function(){
+ frappe.call({
+ method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
+ args: {
+ "document_name": frm.doc.name,
+ },
+ callback: function(r) {
+ if(r && r.message) {
+ frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
+ }
+ else{
+ frappe.msgprint('No Records for these settings.')
+ }
+ }
+ });
+ });
+ frm.add_custom_button('Download',function(){
+ var url = frappe.urllib.get_full_url(
+ '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
+ + 'document_name='+encodeURIComponent(frm.doc.name))
+ $.ajax({
+ url: url,
+ type: 'GET',
+ success: function(result) {
+ if(jQuery.isEmptyObject(result)){
+ frappe.msgprint('No Records for these settings.');
+ }
+ else{
+ window.location = url;
+ }
+ }
+ });
+ });
+ }
+ },
+ onload: function(frm) {
+ frm.set_query('currency', function(){
+ return {
+ filters: {
+ 'enabled': 1
+ }
+ }
+ });
+ if(frm.doc.__islocal){
+ frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
+ frm.set_value('to_date', frappe.datetime.get_today());
+ }
+ },
+ customer_collection: function(frm){
+ frm.set_value('collection_name', '');
+ if(frm.doc.customer_collection){
+ frm.get_field('collection_name').set_label(frm.doc.customer_collection);
+ }
+ },
+ frequency: function(frm){
+ if(frm.doc.frequency != ''){
+ frm.set_value('start_date', frappe.datetime.get_today());
+ }
+ else{
+ frm.set_value('start_date', '');
+ }
+ },
+ fetch_customers: function(frm){
+ if(frm.doc.collection_name){
+ frappe.call({
+ method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.fetch_customers",
+ args: {
+ 'customer_collection': frm.doc.customer_collection,
+ 'collection_name': frm.doc.collection_name,
+ 'primary_mandatory': frm.doc.primary_mandatory
+ },
+ callback: function(r) {
+ if(!r.exc) {
+ if(r.message.length){
+ frm.clear_table('customers');
+ for (const customer of r.message){
+ var row = frm.add_child('customers');
+ row.customer = customer.name;
+ row.primary_email = customer.primary_email;
+ row.billing_email = customer.billing_email;
+ }
+ frm.refresh_field('customers');
+ }
+ else{
+ frappe.msgprint('No Customers found with selected options.');
+ }
+ }
+ }
+ });
+ }
+ else {
+ frappe.throw('Enter ' + frm.doc.customer_collection + ' name.');
+ }
+ }
+});
+
+frappe.ui.form.on('Process Statement Of Accounts Customer', {
+ customer: function(frm, cdt, cdn){
+ var row = locals[cdt][cdn];
+ if (!row.customer){
+ return;
+ }
+ frappe.call({
+ method: 'erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails',
+ args: {
+ 'customer_name': row.customer,
+ 'primary_mandatory': frm.doc.primary_mandatory
+ },
+ callback: function(r){
+ if(!r.exe){
+ if(r.message.length){
+ frappe.model.set_value(cdt, cdn, "primary_email", r.message[0])
+ frappe.model.set_value(cdt, cdn, "billing_email", r.message[1])
+ }
+ else {
+ return
+ }
+ }
+ }
+ })
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
new file mode 100644
index 0000000..4be0e2e
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -0,0 +1,310 @@
+{
+ "actions": [],
+ "allow_workflow": 1,
+ "autoname": "Prompt",
+ "creation": "2020-05-22 16:46:18.712954",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_11",
+ "from_date",
+ "company",
+ "account",
+ "group_by",
+ "cost_center",
+ "column_break_14",
+ "to_date",
+ "finance_book",
+ "currency",
+ "project",
+ "section_break_3",
+ "customer_collection",
+ "collection_name",
+ "fetch_customers",
+ "column_break_6",
+ "primary_mandatory",
+ "column_break_17",
+ "customers",
+ "preferences",
+ "orientation",
+ "section_break_14",
+ "include_ageing",
+ "ageing_based_on",
+ "section_break_1",
+ "enable_auto_email",
+ "section_break_18",
+ "frequency",
+ "filter_duration",
+ "column_break_21",
+ "start_date",
+ "section_break_33",
+ "subject",
+ "column_break_28",
+ "cc_to",
+ "section_break_30",
+ "body",
+ "help_text"
+ ],
+ "fields": [
+ {
+ "fieldname": "frequency",
+ "fieldtype": "Select",
+ "label": "Frequency",
+ "options": "Weekly\nMonthly\nQuarterly"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.enable_auto_email == 0;",
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date",
+ "mandatory_depends_on": "eval:doc.frequency == '';"
+ },
+ {
+ "depends_on": "eval:doc.enable_auto_email == 0;",
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date",
+ "mandatory_depends_on": "eval:doc.frequency == '';"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Table MultiSelect",
+ "label": "Cost Center",
+ "options": "PSOA Cost Center"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Table MultiSelect",
+ "label": "Project",
+ "options": "PSOA Project"
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break",
+ "label": "Customers"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "label": "General Ledger Filters"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "customer_collection",
+ "fieldtype": "Select",
+ "label": "Select Customers By",
+ "options": "\nCustomer Group\nTerritory\nSales Partner\nSales Person"
+ },
+ {
+ "depends_on": "eval: doc.customer_collection !== ''",
+ "fieldname": "collection_name",
+ "fieldtype": "Dynamic Link",
+ "label": "Recipient",
+ "options": "customer_collection"
+ },
+ {
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break",
+ "label": "Email Settings"
+ },
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "label": "Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "finance_book",
+ "fieldtype": "Link",
+ "label": "Finance Book",
+ "options": "Finance Book"
+ },
+ {
+ "fieldname": "preferences",
+ "fieldtype": "Section Break",
+ "label": "Print Preferences"
+ },
+ {
+ "fieldname": "orientation",
+ "fieldtype": "Select",
+ "label": "Orientation",
+ "options": "Landscape\nPortrait"
+ },
+ {
+ "default": "Today",
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date"
+ },
+ {
+ "default": "Group by Voucher (Consolidated)",
+ "fieldname": "group_by",
+ "fieldtype": "Select",
+ "label": "Group By",
+ "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "default": "0",
+ "fieldname": "include_ageing",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Include Ageing Summary"
+ },
+ {
+ "default": "Due Date",
+ "depends_on": "eval:doc.include_ageing === 1",
+ "fieldname": "ageing_based_on",
+ "fieldtype": "Select",
+ "label": "Ageing Based On",
+ "options": "Due Date\nPosting Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_auto_email",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Enable Auto Email"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Column Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "eval: doc.enable_auto_email ==1",
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.customer_collection !== ''",
+ "fieldname": "fetch_customers",
+ "fieldtype": "Button",
+ "label": "Fetch Customers",
+ "options": "fetch_customers",
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "primary_mandatory",
+ "fieldtype": "Check",
+ "label": "Send To Primary Contact"
+ },
+ {
+ "fieldname": "cc_to",
+ "fieldtype": "Link",
+ "label": "CC To",
+ "options": "User"
+ },
+ {
+ "default": "1",
+ "fieldname": "filter_duration",
+ "fieldtype": "Int",
+ "label": "Filter Duration (Months)"
+ },
+ {
+ "fieldname": "customers",
+ "fieldtype": "Table",
+ "label": "Customers",
+ "options": "Process Statement Of Accounts Customer",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_30",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "section_break_33",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "help_text",
+ "fieldtype": "HTML",
+ "label": "Help Text",
+ "options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
+ },
+ {
+ "fieldname": "subject",
+ "fieldtype": "Data",
+ "label": "Subject"
+ },
+ {
+ "fieldname": "body",
+ "fieldtype": "Text Editor",
+ "label": "Body"
+ }
+ ],
+ "links": [],
+ "modified": "2020-08-08 08:47:09.185728",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Statement Of Accounts",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
new file mode 100644
index 0000000..d50e4a8
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -0,0 +1,271 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
+from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
+from frappe.core.doctype.communication.email import make
+
+from frappe.utils.print_format import report_to_pdf
+from frappe.utils.pdf import get_pdf
+from frappe.utils import today, add_days, add_months, getdate, format_date
+from frappe.utils.jinja import validate_template
+
+import copy
+from datetime import timedelta
+from frappe.www.printview import get_print_style
+
+class ProcessStatementOfAccounts(Document):
+ def validate(self):
+ if not self.subject:
+ self.subject = 'Statement Of Accounts for {{ customer.name }}'
+ if not self.body:
+ self.body = 'Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.'
+
+ validate_template(self.subject)
+ validate_template(self.body)
+
+ if not self.customers:
+ frappe.throw(frappe._('Customers not selected.'))
+
+ if self.enable_auto_email:
+ self.to_date = self.start_date
+ self.from_date = add_months(self.to_date, -1 * self.filter_duration)
+
+
+def get_report_pdf(doc, consolidated=True):
+ statement_dict = {}
+ aging = ''
+ base_template_path = "frappe/www/printview.html"
+ template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
+
+ for entry in doc.customers:
+ if doc.include_ageing:
+ ageing_filters = frappe._dict({
+ 'company': doc.company,
+ 'report_date': doc.to_date,
+ 'ageing_based_on': doc.ageing_based_on,
+ 'range1': 30,
+ 'range2': 60,
+ 'range3': 90,
+ 'range4': 120,
+ 'customer': entry.customer
+ })
+ col1, aging = get_ageing(ageing_filters)
+ aging[0]['ageing_based_on'] = doc.ageing_based_on
+
+ tax_id = frappe.get_doc('Customer', entry.customer).tax_id
+
+ filters= frappe._dict({
+ 'from_date': doc.from_date,
+ 'to_date': doc.to_date,
+ 'company': doc.company,
+ 'finance_book': doc.finance_book if doc.finance_book else None,
+ "account": doc.account if doc.account else None,
+ 'party_type': 'Customer',
+ 'party': [entry.customer],
+ 'group_by': doc.group_by,
+ 'currency': doc.currency,
+ 'cost_center': [cc.cost_center_name for cc in doc.cost_center],
+ 'project': [p.project_name for p in doc.project],
+ 'show_opening_entries': 0,
+ 'include_default_book_entries': 0,
+ 'show_cancelled_entries': 1,
+ 'tax_id': tax_id if tax_id else None
+ })
+ col, res = get_soa(filters)
+
+ for x in [0, -2, -1]:
+ res[x]['account'] = res[x]['account'].replace("'","")
+
+ if len(res) == 3:
+ continue
+ html = frappe.render_template(template_path, \
+ {"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None})
+ html = frappe.render_template(base_template_path, {"body": html, \
+ "css": get_print_style(), "title": "Statement For " + entry.customer})
+ statement_dict[entry.customer] = html
+ if not bool(statement_dict):
+ return False
+ elif consolidated:
+ result = ''.join(list(statement_dict.values()))
+ return get_pdf(result, {'orientation': doc.orientation})
+ else:
+ for customer, statement_html in statement_dict.items():
+ statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation})
+ return statement_dict
+
+def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
+ fields_dict = {
+ 'Customer Group': 'customer_group',
+ 'Territory': 'territory',
+ }
+ collection = frappe.get_doc(customer_collection, collection_name)
+ selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[
+ ['lft', '>=', collection.lft],
+ ['rgt', '<=', collection.rgt]
+ ],
+ fields=['name'],
+ order_by='lft asc, rgt desc'
+ )]
+ return frappe.get_list('Customer', fields=['name', 'email_id'], \
+ filters=[[fields_dict[customer_collection], 'IN', selected]])
+
+def get_customers_based_on_sales_person(sales_person):
+ lft, rgt = frappe.db.get_value("Sales Person",
+ sales_person, ["lft", "rgt"])
+ records = frappe.db.sql("""
+ select distinct parent, parenttype
+ from `tabSales Team` steam
+ where parenttype = 'Customer'
+ and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
+ """, (lft, rgt), as_dict=1)
+ sales_person_records = frappe._dict()
+ for d in records:
+ sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
+ customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
+ filters=[['name', 'in', list(sales_person_records['Customer'])]])
+ return customers
+
+def get_recipients_and_cc(customer, doc):
+ recipients = []
+ for clist in doc.customers:
+ if clist.customer == customer:
+ recipients.append(clist.billing_email)
+ if doc.primary_mandatory and clist.primary_email:
+ recipients.append(clist.primary_email)
+ cc = []
+ if doc.cc_to != '':
+ try:
+ cc=[frappe.get_value('User', doc.cc_to, 'email')]
+ except:
+ pass
+
+ return recipients, cc
+
+def get_context(customer, doc):
+ template_doc = copy.deepcopy(doc)
+ del template_doc.customers
+ template_doc.from_date = format_date(template_doc.from_date)
+ template_doc.to_date = format_date(template_doc.to_date)
+ return {
+ 'doc': template_doc,
+ 'customer': frappe.get_doc('Customer', customer),
+ 'frappe': frappe.utils
+ }
+
+@frappe.whitelist()
+def fetch_customers(customer_collection, collection_name, primary_mandatory):
+ customer_list = []
+ customers = []
+
+ if customer_collection == 'Sales Person':
+ customers = get_customers_based_on_sales_person(collection_name)
+ if not bool(customers):
+ frappe.throw('No Customers found with selected options.')
+ else:
+ if customer_collection == 'Sales Partner':
+ customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
+ filters=[['default_sales_partner', '=', collection_name]])
+ else:
+ customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name)
+
+ for customer in customers:
+ primary_email = customer.get('email_id') or ''
+ billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
+
+ if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
+ continue
+
+ customer_list.append({
+ 'name': customer.name,
+ 'primary_email': primary_email,
+ 'billing_email': billing_email
+ })
+ return customer_list
+
+@frappe.whitelist()
+def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
+ billing_email = frappe.db.sql("""
+ SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
+ WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
+ c.is_billing_contact=1 \
+ order by c.creation desc""")
+
+ if len(billing_email) == 0 or (billing_email[0][0] is None):
+ if billing_and_primary:
+ frappe.throw('No billing email found for customer: '+ customer_name)
+ else:
+ return ''
+
+ if billing_and_primary:
+ primary_email = frappe.get_value('Customer', customer_name, 'email_id')
+ if primary_email is None and int(primary_mandatory):
+ frappe.throw('No primary email found for customer: '+ customer_name)
+ return [primary_email or '', billing_email[0][0]]
+ else:
+ return billing_email[0][0] or ''
+
+@frappe.whitelist()
+def download_statements(document_name):
+ doc = frappe.get_doc('Process Statement Of Accounts', document_name)
+ report = get_report_pdf(doc)
+ if report:
+ frappe.local.response.filename = doc.name + '.pdf'
+ frappe.local.response.filecontent = report
+ frappe.local.response.type = "download"
+
+@frappe.whitelist()
+def send_emails(document_name, from_scheduler=False):
+ doc = frappe.get_doc('Process Statement Of Accounts', document_name)
+ report = get_report_pdf(doc, consolidated=False)
+
+ if report:
+ for customer, report_pdf in report.items():
+ attachments = [{
+ 'fname': customer + '.pdf',
+ 'fcontent': report_pdf
+ }]
+
+ recipients, cc = get_recipients_and_cc(customer, doc)
+ context = get_context(customer, doc)
+ subject = frappe.render_template(doc.subject, context)
+ message = frappe.render_template(doc.body, context)
+
+ frappe.enqueue(
+ queue='short',
+ method=frappe.sendmail,
+ recipients=recipients,
+ sender=frappe.session.user,
+ cc=cc,
+ subject=subject,
+ message=message,
+ now=True,
+ reference_doctype='Process Statement Of Accounts',
+ reference_name=document_name,
+ attachments=attachments
+ )
+
+ if doc.enable_auto_email and from_scheduler:
+ new_to_date = getdate(today())
+ if doc.frequency == 'Weekly':
+ new_to_date = add_days(new_to_date, 7)
+ else:
+ new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3)
+ new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
+ doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now()))
+ doc.db_set('to_date', new_to_date, commit=True)
+ doc.db_set('from_date', new_from_date, commit=True)
+ return True
+ else:
+ return False
+
+@frappe.whitelist()
+def send_auto_email():
+ selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1})
+ for entry in selected:
+ send_emails(entry.name, from_scheduler=True)
+ return True
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
new file mode 100644
index 0000000..30efbb3
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestProcessStatementOfAccounts(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
new file mode 100644
index 0000000..dd04dc1
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
@@ -0,0 +1,47 @@
+{
+ "actions": [],
+ "allow_workflow": 1,
+ "creation": "2020-08-03 16:35:21.852178",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "customer",
+ "billing_email",
+ "primary_email"
+ ],
+ "fields": [
+ {
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer",
+ "reqd": 1
+ },
+ {
+ "fieldname": "primary_email",
+ "fieldtype": "Read Only",
+ "in_list_view": 1,
+ "label": "Primary Contact Email"
+ },
+ {
+ "fieldname": "billing_email",
+ "fieldtype": "Read Only",
+ "in_list_view": 1,
+ "label": "Billing Email"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-03 22:55:38.875601",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Statement Of Accounts Customer",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py
new file mode 100644
index 0000000..1a76010
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ProcessStatementOfAccountsCustomer(Document):
+ pass
diff --git a/erpnext/accounts/doctype/psoa_cost_center/__init__.py b/erpnext/accounts/doctype/psoa_cost_center/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_cost_center/__init__.py
diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json
new file mode 100644
index 0000000..e292b60
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json
@@ -0,0 +1,30 @@
+{
+ "actions": [],
+ "creation": "2020-08-03 16:56:45.744905",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "cost_center_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "cost_center_name",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-03 16:56:45.744905",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "PSOA Cost Center",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py
new file mode 100644
index 0000000..0aeef3e
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PSOACostCenter(Document):
+ pass
diff --git a/erpnext/accounts/doctype/psoa_project/__init__.py b/erpnext/accounts/doctype/psoa_project/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_project/__init__.py
diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.json b/erpnext/accounts/doctype/psoa_project/psoa_project.json
new file mode 100644
index 0000000..20a03ee
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_project/psoa_project.json
@@ -0,0 +1,30 @@
+{
+ "actions": [],
+ "creation": "2020-08-03 16:52:14.731978",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "project_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "project_name",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-03 16:53:39.219736",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "PSOA Project",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.py b/erpnext/accounts/doctype/psoa_project/psoa_project.py
new file mode 100644
index 0000000..f4a5dee
--- /dev/null
+++ b/erpnext/accounts/doctype/psoa_project/psoa_project.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PSOAProject(Document):
+ pass
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 95a836f..463ad6c 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -322,7 +322,8 @@
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status",
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
- "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status"
+ "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
+ "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",