Merge branch 'develop' into gross-profit-product-bundle

This commit is contained in:
Ganga Manoj 2021-08-31 22:08:10 +05:30 committed by GitHub
commit 82118975e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 9868 additions and 2356 deletions

View File

@ -4,11 +4,7 @@ set -e
cd ~ || exit
sudo apt-get install redis-server
sudo apt install nodejs
sudo apt install npm
sudo apt-get install redis-server libcups2-dev
pip install frappe-bench
@ -32,7 +28,6 @@ wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/w
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
sudo apt-get install libcups2-dev
cd ~/frappe-bench || exit

View File

@ -7,10 +7,13 @@ on:
- '**.md'
workflow_dispatch:
concurrency:
group: patch-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
timeout-minutes: 60
name: Patch Test
@ -31,7 +34,13 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: 3.7
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts

View File

@ -12,9 +12,13 @@ on:
- '**.js'
- '**.md'
concurrency:
group: server-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
@ -43,6 +47,12 @@ jobs:
with:
python-version: 3.7
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
@ -107,7 +117,7 @@ jobs:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v2

View File

@ -1,22 +0,0 @@
name: Frappe Linter
on:
pull_request:
branches:
- develop
- version-12-hotfix
- version-11-hotfix
jobs:
check_translation:
name: Translation Syntax Check
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v1
with:
python-version: 3.6
- name: Validating Translation Syntax
run: |
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
python $GITHUB_WORKSPACE/.github/helper/translation.py $files

View File

@ -6,9 +6,13 @@ on:
- '**.md'
workflow_dispatch:
concurrency:
group: ui-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:

View File

@ -2,8 +2,11 @@ context('Organizational Chart', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
it('navigates to org chart', () => {
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.wait(500);
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {

View File

@ -1,9 +1,14 @@
context('Organizational Chart Mobile', () => {
before(() => {
cy.login();
cy.viewport(375, 667);
cy.visit('/app/website');
});
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({

View File

@ -74,7 +74,7 @@ frappe.ui.form.on('Account', {
});
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
cur_frm.add_custom_button(__('Ledger'), function () {
frm.add_custom_button(__('Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,

View File

@ -6,46 +6,3 @@ frappe.ui.form.on('Accounts Settings', {
}
});
frappe.tour['Accounts Settings'] = [
{
fieldname: "acc_frozen_upto",
title: "Accounts Frozen Upto",
description: __("Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role."),
},
{
fieldname: "frozen_accounts_modifier",
title: "Role Allowed to Set Frozen Accounts & Edit Frozen Entries",
description: __("Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.")
},
{
fieldname: "determine_address_tax_category_from",
title: "Determine Address Tax Category From",
description: __("Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.")
},
{
fieldname: "over_billing_allowance",
title: "Over Billing Allowance Percentage",
description: __("The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.")
},
{
fieldname: "credit_controller",
title: "Credit Controller",
description: __("Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.")
},
{
fieldname: "make_payment_via_journal_entry",
title: "Make Payment via Journal Entry",
description: __("When checked, if user proceeds to make payment from an invoice, the system will open a Journal Entry instead of a Payment Entry.")
},
{
fieldname: "unlink_payment_on_cancellation_of_invoice",
title: "Unlink Payment on Cancellation of Invoice",
description: __("If checked, system will unlink the payment against the respective invoice.")
},
{
fieldname: "unlink_advance_payment_on_cancelation_of_order",
title: "Unlink Advance Payment on Cancellation of Order",
description: __("Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.")
}
];

View File

@ -66,6 +66,7 @@ class JournalEntry(AccountsController):
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
self.update_status_for_full_and_final_statement()
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
@ -83,6 +84,7 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry()
self.update_invoice_discounting()
self.update_status_for_full_and_final_statement()
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
@ -98,6 +100,15 @@ class JournalEntry(AccountsController):
for voucher_no in list(set(order_list)):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def update_status_for_full_and_final_statement(self):
for entry in self.accounts:
if entry.reference_type == "Full and Final Statement":
if self.docstatus == 1:
frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Paid")
elif self.docstatus == 2:
frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Unpaid")
def validate_inter_company_accounts(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
@ -643,7 +654,10 @@ class JournalEntry(AccountsController):
for d in self.accounts:
if d.reference_type=="Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, jv=self.name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.debit)
else:
update_reimbursed_amount(doc, d.debit)
def validate_expense_claim(self):

View File

@ -202,7 +202,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees"
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
},
{
"fieldname": "reference_name",
@ -280,7 +280,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-06-26 14:06:54.833738",
"modified": "2021-08-30 21:27:32.200299",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@ -872,7 +872,7 @@ frappe.ui.form.on('Payment Entry', {
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
+ frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {

View File

@ -75,9 +75,9 @@ class PaymentEntry(AccountsController):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
self.update_donation()
self.update_payment_schedule()
self.set_status()
@ -85,9 +85,9 @@ class PaymentEntry(AccountsController):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
self.update_donation(cancel=1)
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
@ -831,7 +831,10 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.reference_doctype=="Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.allocated_amount)
else:
update_reimbursed_amount(doc, d.allocated_amount)
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:

View File

@ -2,46 +2,10 @@
// For license information, please see license.txt
frappe.provide("erpnext.accounts");
frappe.ui.form.on("Payment Reconciliation Payment", {
invoice_number: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if(row.invoice_number) {
var parts = row.invoice_number.split(' | ');
var invoice_type = parts[0];
var invoice_number = parts[1];
var invoice_amount = frm.doc.invoices.filter(function(d) {
return d.invoice_type === invoice_type && d.invoice_number === invoice_number;
})[0].outstanding_amount;
frappe.model.set_value(cdt, cdn, "allocated_amount", Math.min(invoice_amount, row.amount));
frm.call({
doc: frm.doc,
method: 'get_difference_amount',
args: {
child_row: row
},
callback: function(r, rt) {
if(r.message) {
frappe.model.set_value(cdt, cdn,
"difference_amount", r.message);
}
}
});
}
}
});
erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller {
onload() {
var me = this;
this.frm.set_query("party", function() {
check_mandatory(me.frm);
});
this.frm.set_query("party_type", function() {
return {
"filters": {
@ -88,15 +52,36 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
refresh() {
this.frm.disable_save();
this.toggle_primary_action();
if (this.frm.doc.receivable_payable_account) {
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
this.frm.trigger("get_unreconciled_entries")
);
}
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
this.frm.add_custom_button(__('Allocate'), () =>
this.frm.trigger("allocate")
);
}
if (this.frm.doc.allocation.length) {
this.frm.add_custom_button(__('Reconcile'), () =>
this.frm.trigger("reconcile")
);
}
}
onload_post_render() {
this.toggle_primary_action();
company() {
var me = this;
this.frm.set_value('receivable_payable_account', '');
me.frm.clear_table("allocation");
me.frm.clear_table("invoices");
me.frm.clear_table("payments");
me.frm.refresh_fields();
me.frm.trigger('party');
}
party() {
var me = this
var me = this;
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
@ -109,6 +94,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
if (!r.exc && r.message) {
me.frm.set_value("receivable_payable_account", r.message);
}
me.frm.refresh();
}
});
}
@ -120,16 +106,41 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
doc: me.frm.doc,
method: 'get_unreconciled_entries',
callback: function(r, rt) {
me.set_invoice_options();
me.toggle_primary_action();
if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) {
frappe.throw({message: __("No invoice and payment records found for this party")});
}
me.frm.refresh();
}
});
}
allocate() {
var me = this;
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
if (!(payments.length)) {
payments = me.frm.doc.payments;
}
let invoices = me.frm.fields_dict.invoices.grid.get_selected_children();
if (!(invoices.length)) {
invoices = me.frm.doc.invoices;
}
return me.frm.call({
doc: me.frm.doc,
method: 'allocate_entries',
args: {
payments: payments,
invoices: invoices
},
callback: function() {
me.frm.refresh();
}
});
}
reconcile() {
var me = this;
var show_dialog = me.frm.doc.payments.filter(d => d.difference_amount && !d.difference_account);
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
if (show_dialog && show_dialog.length) {
@ -138,7 +149,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
title: __("Select Difference Account"),
fields: [
{
fieldname: "payments", fieldtype: "Table", label: __("Payments"),
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
data: this.data, in_place_edit: true,
get_data: () => {
return this.data;
@ -179,10 +190,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
],
primary_action: function() {
const args = dialog.get_values()["payments"];
const args = dialog.get_values()["allocation"];
args.forEach(d => {
frappe.model.set_value("Payment Reconciliation Payment", d.docname,
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
"difference_account", d.difference_account);
});
@ -192,9 +203,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
primary_action_label: __('Reconcile Entries')
});
this.frm.doc.payments.forEach(d => {
this.frm.doc.allocation.forEach(d => {
if (d.difference_amount && !d.difference_account) {
dialog.fields_dict.payments.df.data.push({
dialog.fields_dict.allocation.df.data.push({
'docname': d.name,
'reference_name': d.reference_name,
'difference_amount': d.difference_amount,
@ -203,8 +214,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
});
this.data = dialog.fields_dict.payments.df.data;
dialog.fields_dict.payments.grid.refresh();
this.data = dialog.fields_dict.allocation.df.data;
dialog.fields_dict.allocation.grid.refresh();
dialog.show();
} else {
this.reconcile_payment_entries();
@ -218,48 +229,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
doc: me.frm.doc,
method: 'reconcile',
callback: function(r, rt) {
me.set_invoice_options();
me.toggle_primary_action();
me.frm.clear_table("allocation");
me.frm.refresh_fields();
me.frm.refresh();
}
});
}
set_invoice_options() {
var me = this;
var invoices = [];
$.each(me.frm.doc.invoices || [], function(i, row) {
if (row.invoice_number && !in_list(invoices, row.invoice_number))
invoices.push(row.invoice_type + " | " + row.invoice_number);
});
if (invoices) {
this.frm.fields_dict.payments.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n")
);
$.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;
});
}
refresh_field("payments");
}
toggle_primary_action() {
if ((this.frm.doc.payments || []).length) {
this.frm.fields_dict.reconcile.$input
&& this.frm.fields_dict.reconcile.$input.addClass("btn-primary");
this.frm.fields_dict.get_unreconciled_entries.$input
&& this.frm.fields_dict.get_unreconciled_entries.$input.removeClass("btn-primary");
} else {
this.frm.fields_dict.reconcile.$input
&& this.frm.fields_dict.reconcile.$input.removeClass("btn-primary");
this.frm.fields_dict.get_unreconciled_entries.$input
&& this.frm.fields_dict.get_unreconciled_entries.$input.addClass("btn-primary");
}
}
};
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));

View File

@ -1,622 +1,206 @@
{
"allow_copy": 1,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-07-09 12:04:51.681583",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"allow_copy": 1,
"creation": "2014-07-09 12:04:51.681583",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"company",
"party_type",
"column_break_4",
"party",
"receivable_payable_account",
"col_break1",
"from_invoice_date",
"to_invoice_date",
"minimum_invoice_amount",
"maximum_invoice_amount",
"invoice_limit",
"column_break_13",
"from_payment_date",
"to_payment_date",
"minimum_payment_amount",
"maximum_payment_amount",
"payment_limit",
"bank_cash_account",
"sec_break1",
"invoices",
"column_break_15",
"payments",
"sec_break2",
"allocation"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "party_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Party Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "party_type",
"fieldtype": "Link",
"label": "Party Type",
"options": "DocType",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "party",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Party",
"length": 0,
"no_copy": 0,
"options": "party_type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:doc.party_type",
"fieldname": "party",
"fieldtype": "Dynamic Link",
"label": "Party",
"options": "party_type",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "receivable_payable_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Receivable / Payable Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:doc.company && doc.party",
"fieldname": "receivable_payable_account",
"fieldtype": "Link",
"label": "Receivable / Payable Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "bank_cash_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Bank / Cash Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"description": "This filter will be applied to Journal Entry.",
"fieldname": "bank_cash_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Bank / Cash Account",
"options": "Account"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"collapsible": 1,
"collapsible_depends_on": "eval: doc.invoices.length == 0",
"depends_on": "eval:doc.receivable_payable_account",
"fieldname": "col_break1",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "From Invoice Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:(doc.payments).length || (doc.invoices).length",
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"label": "Unreconciled Entries"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "To Invoice Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "payments",
"fieldtype": "Table",
"label": "Payments",
"options": "Payment Reconciliation Payment"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "minimum_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Minimum Invoice Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"depends_on": "allocation",
"fieldname": "sec_break2",
"fieldtype": "Section Break",
"label": "Allocated Entries"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maximum_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Maximum Invoice Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
"options": "Payment Reconciliation Invoice"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "limit",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Limit",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_unreconciled_entries",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Get Unreconciled Entries",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "allocation",
"fieldtype": "Table",
"label": "Allocation",
"options": "Payment Reconciliation Allocation"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Unreconciled Payment Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payments",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payments",
"length": 0,
"no_copy": 0,
"options": "Payment Reconciliation Payment",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "from_invoice_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "From Invoice Date"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reconcile",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reconcile",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "to_invoice_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "To Invoice Date"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sec_break2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoice/Journal Entry Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "minimum_invoice_amount",
"fieldtype": "Currency",
"label": "Minimum Invoice Amount"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "invoices",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoices",
"length": 0,
"no_copy": 0,
"options": "Payment Reconciliation Invoice",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "invoice_limit",
"fieldtype": "Int",
"label": "Invoice Limit"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "from_payment_date",
"fieldtype": "Date",
"label": "From Payment Date"
},
{
"fieldname": "to_payment_date",
"fieldtype": "Date",
"label": "To Payment Date"
},
{
"fieldname": "minimum_payment_amount",
"fieldtype": "Currency",
"label": "Minimum Payment Amount"
},
{
"fieldname": "maximum_payment_amount",
"fieldtype": "Currency",
"label": "Maximum Payment Amount"
},
{
"fieldname": "payment_limit",
"fieldtype": "Int",
"label": "Payment Limit"
},
{
"fieldname": "maximum_invoice_amount",
"fieldtype": "Currency",
"label": "Maximum Invoice Amount"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-01-15 17:42:21.135214",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
"name_case": "",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
"modified": "2021-08-30 13:05:51.977861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe.utils import flt, today
from frappe.utils import flt, today, getdate, nowdate
from frappe import msgprint, _
from frappe.model.document import Document
from erpnext.accounts.utils import (get_outstanding_invoices,
@ -27,24 +27,32 @@ class PaymentReconciliation(Document):
else:
dr_or_cr_notes = []
self.add_payment_entries(payment_entries + journal_entries + dr_or_cr_notes)
non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes
if self.payment_limit:
non_reconciled_payments = non_reconciled_payments[:self.payment_limit]
non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate()))
self.add_payment_entries(non_reconciled_payments)
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
payment_entries = get_advance_payment_entries(self.party_type, self.party,
self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.limit)
self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit,
condition=condition)
return payment_entries
def get_jv_entries(self):
condition = self.get_conditions()
dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
else "debit_in_account_currency")
bank_account_condition = "t2.against_account like %(bank_cash_account)s" \
if self.bank_cash_account else "1=1"
limit_cond = "limit %s" % self.limit if self.limit else ""
journal_entries = frappe.db.sql("""
select
"Journal Entry" as reference_type, t1.name as reference_name,
@ -56,7 +64,7 @@ class PaymentReconciliation(Document):
where
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
and t2.party_type = %(party_type)s and t2.party = %(party)s
and t2.account = %(account)s and {dr_or_cr} > 0
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
and (t2.reference_type is null or t2.reference_type = '' or
(t2.reference_type in ('Sales Order', 'Purchase Order')
and t2.reference_name is not null and t2.reference_name != ''))
@ -65,11 +73,11 @@ class PaymentReconciliation(Document):
THEN 1=1
ELSE {bank_account_condition}
END)
order by t1.posting_date {limit_cond}
order by t1.posting_date
""".format(**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"limit_cond": limit_cond
"condition": condition
}), {
"party_type": self.party_type,
"party": self.party,
@ -80,6 +88,7 @@ class PaymentReconciliation(Document):
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
dr_or_cr = ("credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
@ -90,7 +99,7 @@ class PaymentReconciliation(Document):
if self.party_type == 'Customer' else "Purchase Invoice")
return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE
@ -100,15 +109,17 @@ class PaymentReconciliation(Document):
and gl.against_voucher_type = %(voucher_type)s
and doc.docstatus = 1 and gl.party = %(party)s
and gl.party_type = %(party_type)s and gl.account = %(account)s
and gl.is_cancelled = 0
and gl.is_cancelled = 0 {condition}
GROUP BY doc.name
Having
amount > 0
ORDER BY doc.posting_date
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type)),
party_type_field=frappe.scrub(self.party_type),
condition=condition or ""),
{
'party': self.party,
'party_type': self.party_type,
@ -116,22 +127,23 @@ class PaymentReconciliation(Document):
'account': self.receivable_payable_account
}, as_dict=1)
def add_payment_entries(self, entries):
def add_payment_entries(self, non_reconciled_payments):
self.set('payments', [])
for e in entries:
for payment in non_reconciled_payments:
row = self.append('payments', {})
row.update(e)
row.update(payment)
def get_invoice_entries(self):
#Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.check_condition()
condition = self.get_conditions(get_invoices=True)
non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party,
self.receivable_payable_account, condition=condition)
if self.limit:
non_reconciled_invoices = non_reconciled_invoices[:self.limit]
if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit]
self.add_invoice_entries(non_reconciled_invoices)
@ -139,41 +151,78 @@ class PaymentReconciliation(Document):
#Populate 'invoices' with JVs and Invoices to reconcile against
self.set('invoices', [])
for e in non_reconciled_invoices:
ent = self.append('invoices', {})
ent.invoice_type = e.get('voucher_type')
ent.invoice_number = e.get('voucher_no')
ent.invoice_date = e.get('posting_date')
ent.amount = flt(e.get('invoice_amount'))
ent.currency = e.get('currency')
ent.outstanding_amount = e.get('outstanding_amount')
for entry in non_reconciled_invoices:
inv = self.append('invoices', {})
inv.invoice_type = entry.get('voucher_type')
inv.invoice_number = entry.get('voucher_no')
inv.invoice_date = entry.get('posting_date')
inv.amount = flt(entry.get('invoice_amount'))
inv.currency = entry.get('currency')
inv.outstanding_amount = flt(entry.get('outstanding_amount'))
@frappe.whitelist()
def reconcile(self, args):
for e in self.get('payments'):
e.invoice_type = None
if e.invoice_number and " | " in e.invoice_number:
e.invoice_type, e.invoice_number = e.invoice_number.split(" | ")
def allocate_entries(self, args):
self.validate_entries()
entries = []
for pay in args.get('payments'):
pay.update({'unreconciled_amount': pay.get('amount')})
for inv in args.get('invoices'):
if pay.get('amount') >= inv.get('outstanding_amount'):
res = self.get_allocated_entry(pay, inv, inv['outstanding_amount'])
pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount'))
inv['outstanding_amount'] = 0
else:
res = self.get_allocated_entry(pay, inv, pay['amount'])
inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount'))
pay['amount'] = 0
if pay.get('amount') == 0:
entries.append(res)
break
elif inv.get('outstanding_amount') == 0:
entries.append(res)
continue
else:
break
self.get_invoice_entries()
self.validate_invoice()
self.set('allocation', [])
for entry in entries:
if entry['allocated_amount'] != 0:
row = self.append('allocation', {})
row.update(entry)
def get_allocated_entry(self, pay, inv, allocated_amount):
return frappe._dict({
'reference_type': pay.get('reference_type'),
'reference_name': pay.get('reference_name'),
'reference_row': pay.get('reference_row'),
'invoice_type': inv.get('invoice_type'),
'invoice_number': inv.get('invoice_number'),
'unreconciled_amount': pay.get('unreconciled_amount'),
'amount': pay.get('amount'),
'allocated_amount': allocated_amount,
'difference_amount': pay.get('difference_amount')
})
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
dr_or_cr = ("credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
lst = []
entry_list = []
dr_or_cr_notes = []
for e in self.get('payments'):
for row in self.get('allocation'):
reconciled_entry = []
if e.invoice_number and e.allocated_amount:
if e.reference_type in ['Sales Invoice', 'Purchase Invoice']:
if row.invoice_number and row.allocated_amount:
if row.reference_type in ['Sales Invoice', 'Purchase Invoice']:
reconciled_entry = dr_or_cr_notes
else:
reconciled_entry = lst
reconciled_entry = entry_list
reconciled_entry.append(self.get_payment_details(e, dr_or_cr))
reconciled_entry.append(self.get_payment_details(row, dr_or_cr))
if lst:
reconcile_against_document(lst)
if entry_list:
reconcile_against_document(entry_list)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
@ -183,98 +232,104 @@ class PaymentReconciliation(Document):
def get_payment_details(self, row, dr_or_cr):
return frappe._dict({
'voucher_type': row.reference_type,
'voucher_no' : row.reference_name,
'voucher_detail_no' : row.reference_row,
'against_voucher_type' : row.invoice_type,
'against_voucher' : row.invoice_number,
'voucher_type': row.get('reference_type'),
'voucher_no' : row.get('reference_name'),
'voucher_detail_no' : row.get('reference_row'),
'against_voucher_type' : row.get('invoice_type'),
'against_voucher' : row.get('invoice_number'),
'account' : self.receivable_payable_account,
'party_type': self.party_type,
'party': self.party,
'is_advance' : row.is_advance,
'is_advance' : row.get('is_advance'),
'dr_or_cr' : dr_or_cr,
'unadjusted_amount' : flt(row.amount),
'allocated_amount' : flt(row.allocated_amount),
'difference_amount': row.difference_amount,
'difference_account': row.difference_account
'unreconciled_amount': flt(row.get('unreconciled_amount')),
'unadjusted_amount' : flt(row.get('amount')),
'allocated_amount' : flt(row.get('allocated_amount')),
'difference_amount': flt(row.get('difference_amount')),
'difference_account': row.get('difference_account')
})
@frappe.whitelist()
def get_difference_amount(self, child_row):
if child_row.get("reference_type") != 'Payment Entry': return
child_row = frappe._dict(child_row)
if child_row.invoice_number and " | " in child_row.invoice_number:
child_row.invoice_type, child_row.invoice_number = child_row.invoice_number.split(" | ")
dr_or_cr = ("credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
row = self.get_payment_details(child_row, dr_or_cr)
doc = frappe.get_doc(row.voucher_type, row.voucher_no)
update_reference_in_payment_entry(row, doc, do_not_save=True)
return doc.difference_amount
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
def validate_invoice(self):
def validate_entries(self):
if not self.get("invoices"):
frappe.throw(_("No records found in the Invoice table"))
frappe.throw(_("No records found in the Invoices table"))
if not self.get("payments"):
frappe.throw(_("No records found in the Payment table"))
frappe.throw(_("No records found in the Payments table"))
def validate_allocation(self):
unreconciled_invoices = frappe._dict()
for d in self.get("invoices"):
unreconciled_invoices.setdefault(d.invoice_type, {}).setdefault(d.invoice_number, d.outstanding_amount)
for inv in self.get("invoices"):
unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount)
invoices_to_reconcile = []
for p in self.get("payments"):
if p.invoice_type and p.invoice_number and p.allocated_amount:
invoices_to_reconcile.append(p.invoice_number)
for row in self.get("allocation"):
if row.invoice_type and row.invoice_number and row.allocated_amount:
invoices_to_reconcile.append(row.invoice_number)
if p.invoice_number not in unreconciled_invoices.get(p.invoice_type, {}):
frappe.throw(_("{0}: {1} not found in Invoice Details table")
.format(p.invoice_type, p.invoice_number))
if flt(row.amount) - flt(row.allocated_amount) < 0:
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}")
.format(row.idx, row.allocated_amount, row.amount))
if flt(p.allocated_amount) > flt(p.amount):
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2}")
.format(p.idx, p.allocated_amount, p.amount))
invoice_outstanding = unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number)
if flt(p.allocated_amount) - invoice_outstanding > 0.009:
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2}")
.format(p.idx, p.allocated_amount, invoice_outstanding))
invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number)
if flt(row.allocated_amount) - invoice_outstanding > 0.009:
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}")
.format(row.idx, row.allocated_amount, invoice_outstanding))
if not invoices_to_reconcile:
frappe.throw(_("Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row"))
frappe.throw(_("No records found in Allocation table"))
def check_condition(self):
cond = " and posting_date >= {0}".format(frappe.db.escape(self.from_date)) if self.from_date else ""
cond += " and posting_date <= {0}".format(frappe.db.escape(self.to_date)) if self.to_date else ""
dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
else "credit_in_account_currency")
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
if self.minimum_amount:
cond += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_amount))
if self.maximum_amount:
cond += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_amount))
if get_invoices:
condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else ""
condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else ""
dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
else "credit_in_account_currency")
return cond
if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
if self.maximum_invoice_amount:
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
else "gl.credit_in_account_currency")
if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
if self.maximum_invoice_amount:
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
else:
condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
if self.minimum_payment_amount:
condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
if self.maximum_payment_amount:
condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
return condition
def reconcile_dr_cr_note(dr_cr_notes, company):
for d in dr_cr_notes:
for inv in dr_cr_notes:
voucher_type = ('Credit Note'
if d.voucher_type == 'Sales Invoice' else 'Debit Note')
if inv.voucher_type == 'Sales Invoice' else 'Debit Note')
reconcile_dr_or_cr = ('debit_in_account_currency'
if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
company_currency = erpnext.get_company_currency(company)
@ -283,25 +338,25 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
"voucher_type": voucher_type,
"posting_date": today(),
"company": company,
"multi_currency": 1 if d.currency != company_currency else 0,
"multi_currency": 1 if inv.currency != company_currency else 0,
"accounts": [
{
'account': d.account,
'party': d.party,
'party_type': d.party_type,
d.dr_or_cr: abs(d.allocated_amount),
'reference_type': d.against_voucher_type,
'reference_name': d.against_voucher,
'account': inv.account,
'party': inv.party,
'party_type': inv.party_type,
inv.dr_or_cr: abs(inv.allocated_amount),
'reference_type': inv.against_voucher_type,
'reference_name': inv.against_voucher,
'cost_center': erpnext.get_default_cost_center(company)
},
{
'account': d.account,
'party': d.party,
'party_type': d.party_type,
reconcile_dr_or_cr: (abs(d.allocated_amount)
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
'reference_type': d.voucher_type,
'reference_name': d.voucher_no,
'account': inv.account,
'party': inv.party,
'party_type': inv.party_type,
reconcile_dr_or_cr: (abs(inv.allocated_amount)
if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)),
'reference_type': inv.voucher_type,
'reference_name': inv.voucher_no,
'cost_center': erpnext.get_default_cost_center(company)
}
]

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestPaymentReconciliation(unittest.TestCase):
pass

View File

@ -0,0 +1,137 @@
{
"actions": [],
"creation": "2021-08-16 17:04:40.185167",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"column_break_3",
"invoice_type",
"invoice_number",
"section_break_6",
"allocated_amount",
"unreconciled_amount",
"amount",
"column_break_8",
"is_advance",
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account"
],
"fields": [
{
"fieldname": "invoice_number",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice Number",
"options": "invoice_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "Currency",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "difference_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Difference Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "is_advance",
"fieldtype": "Data",
"hidden": 1,
"label": "Is Advance",
"read_only": 1
},
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "invoice_type",
"fieldtype": "Link",
"label": "Invoice Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "unreconciled_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "Currency",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-08-30 10:58:42.665107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentReconciliationAllocation(Document):
pass

View File

@ -44,7 +44,6 @@
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1
@ -67,7 +66,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-07-19 18:12:27.964073",
"modified": "2021-08-24 22:42:40.923179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",

View File

@ -11,11 +11,7 @@
"is_advance",
"reference_row",
"col_break1",
"invoice_number",
"amount",
"allocated_amount",
"section_break_10",
"difference_account",
"difference_amount",
"sec_break1",
"remark",
@ -41,6 +37,7 @@
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"read_only": 1
},
@ -62,14 +59,6 @@
"fieldname": "col_break1",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "invoice_number",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Invoice Number",
"reqd": 1
},
{
"columns": 2,
"fieldname": "amount",
@ -79,15 +68,6 @@
"options": "currency",
"read_only": 1
},
{
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated amount",
"options": "currency",
"reqd": 1
},
{
"fieldname": "sec_break1",
"fieldtype": "Section Break"
@ -95,41 +75,27 @@
{
"fieldname": "remark",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Remark",
"read_only": 1
},
{
"columns": 2,
"fieldname": "difference_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Difference Account",
"options": "Account"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"label": "Difference Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"label": "Difference Amount",
"options": "currency",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-07-19 18:12:41.682347",
"modified": "2021-08-30 10:51:48.140062",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",

View File

@ -5,25 +5,3 @@ cur_frm.cscript.tax_table = "Sales Taxes and Charges";
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.tour['Sales Taxes and Charges Template'] = [
{
fieldname: "title",
title: __("Title"),
description: __("A name by which you will identify this template. You can change this later."),
},
{
fieldname: "company",
title: __("Company"),
description: __("Company for which this tax template will be applicable"),
},
{
fieldname: "is_default",
title: __("Is this Default?"),
description: __("Set this template as the default for all sales transactions"),
},
{
fieldname: "taxes",
title: __("Taxes Table"),
description: __("You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount."),
}
];

View File

@ -0,0 +1,113 @@
{
"creation": "2021-06-29 17:00:18.273054",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-06-29 17:00:26.145996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
"owner": "Administrator",
"reference_doctype": "Accounts Settings",
"save_on_complete": 0,
"steps": [
{
"description": "The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.",
"field": "",
"fieldname": "over_billing_allowance",
"fieldtype": "Currency",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Over Billing Allowance (%)",
"parent_field": "",
"position": "Right",
"title": "Over Billing Allowance Percentage"
},
{
"description": "Select the role that is allowed to overbill a transactions.",
"field": "",
"fieldname": "role_allowed_to_over_bill",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Role Allowed to Over Bill ",
"parent_field": "",
"position": "Right",
"title": "Role Allowed to Over Bill"
},
{
"description": "If checked, system will unlink the payment against the respective invoice.",
"field": "",
"fieldname": "unlink_payment_on_cancellation_of_invoice",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Unlink Payment on Cancellation of Invoice",
"parent_field": "",
"position": "Bottom",
"title": "Unlink Payment on Cancellation of Invoice"
},
{
"description": "Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.",
"field": "",
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Unlink Advance Payment on Cancellation of Order",
"parent_field": "",
"position": "Bottom",
"title": "Unlink Advance Payment on Cancellation of Order"
},
{
"description": "Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.",
"field": "",
"fieldname": "determine_address_tax_category_from",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Determine Address Tax Category From",
"parent_field": "",
"position": "Right",
"title": "Determine Address Tax Category From"
},
{
"description": "Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role.",
"field": "",
"fieldname": "acc_frozen_upto",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Accounts Frozen Till Date",
"parent_field": "",
"position": "Right",
"title": "Accounts Frozen Upto"
},
{
"description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.",
"field": "",
"fieldname": "frozen_accounts_modifier",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
"parent_field": "",
"position": "Right",
"title": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries"
},
{
"description": "Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.",
"field": "",
"fieldname": "credit_controller",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Credit Controller",
"parent_field": "",
"position": "Left",
"title": "Credit Controller"
}
],
"title": "Accounts Settings"
}

View File

@ -0,0 +1,96 @@
{
"creation": "2021-06-29 16:31:48.558826",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-06-29 16:31:48.558826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
"owner": "Administrator",
"reference_doctype": "Purchase Invoice",
"save_on_complete": 1,
"steps": [
{
"description": "Select Supplier",
"field": "",
"fieldname": "supplier",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Supplier",
"next_step_condition": "supplier",
"parent_field": "",
"position": "Right",
"title": "Select Supplier"
},
{
"description": "Add items in the table",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Items",
"parent_field": "",
"position": "Bottom",
"title": "List of Items"
},
{
"child_doctype": "Purchase Invoice Item",
"description": "Select an item",
"field": "",
"fieldname": "item_code",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Item",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Select Item"
},
{
"child_doctype": "Purchase Invoice Item",
"description": "Enter the quantity",
"field": "",
"fieldname": "qty",
"fieldtype": "Float",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Accepted Qty",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Enter Quantity"
},
{
"child_doctype": "Purchase Invoice Item",
"description": "Enter rate of the item",
"field": "",
"fieldname": "rate",
"fieldtype": "Currency",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Rate",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Enter Rate"
},
{
"description": "You can add taxes here",
"field": "",
"fieldname": "taxes",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Purchase Taxes and Charges",
"parent_field": "",
"position": "Bottom",
"title": "Select taxes"
}
],
"title": "Purchase Invoice"
}

View File

@ -0,0 +1,65 @@
{
"creation": "2021-08-24 12:28:18.044902",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-24 12:28:18.044902",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
"owner": "Administrator",
"reference_doctype": "Sales Taxes and Charges Template",
"save_on_complete": 0,
"steps": [
{
"description": "A name by which you will identify this template. You can change this later.",
"field": "",
"fieldname": "title",
"fieldtype": "Data",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Title",
"parent_field": "",
"position": "Bottom",
"title": "Title"
},
{
"description": "Company for which this tax template will be applicable",
"field": "",
"fieldname": "company",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Company",
"parent_field": "",
"position": "Bottom",
"title": "Company"
},
{
"description": "Set this template as the default for all sales transactions",
"field": "",
"fieldname": "is_default",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default",
"parent_field": "",
"position": "Bottom",
"title": "Is this Default Tax Template?"
},
{
"description": "You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount.",
"field": "",
"fieldname": "taxes",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Sales Taxes and Charges",
"parent_field": "",
"position": "Bottom",
"title": "Taxes Table"
}
],
"title": "Sales Taxes and Charges Template"
}

View File

@ -13,12 +13,15 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0,
"is_complete": 0,
"modified": "2020-10-30 15:41:15.547225",
"modified": "2021-08-13 11:59:35.690443",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",
"owner": "Administrator",
"steps": [
{
"step": "Company"
},
{
"step": "Chart of Accounts"
},
@ -26,22 +29,19 @@
"step": "Setup Taxes"
},
{
"step": "Create a Product"
"step": "Accounts Settings"
},
{
"step": "Create a Supplier"
"step": "Cost Centers for Report and Budgeting"
},
{
"step": "Create Your First Purchase Invoice"
},
{
"step": "Create a Customer"
"step": "Updating Opening Balances"
},
{
"step": "Create Your First Sales Invoice"
},
{
"step": "Configure Account Settings"
"step": "Financial Statements"
}
],
"subtitle": "Accounts, Invoices, Taxation, and more.",

View File

@ -0,0 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Take a quick walk-through of Accounts Settings",
"creation": "2021-06-29 16:42:03.400731",
"description": "# Account Settings\n\nIn ERPNext, Accounting features are configurable as per your business needs. Accounts Settings is the place to define some of your accounting preferences like:\n\n - Credit Limit and over billing settings\n - Taxation preferences\n - Deferred accounting preferences\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2021-08-13 11:50:06.227835",
"modified_by": "Administrator",
"name": "Accounts Settings",
"owner": "Administrator",
"reference_document": "Accounts Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Accounts Settings",
"validate_action": 1
}

View File

@ -1,10 +1,10 @@
{
"action": "Go to Page",
"action_label": "View Chart of Accounts",
"action": "Watch Video",
"action_label": "Learn more about Chart of Accounts",
"callback_message": "You can continue with the onboarding after exploring this page",
"callback_title": "Awesome Work",
"creation": "2020-05-13 19:58:20.928127",
"description": "# Chart Of Accounts\n\nThe Chart of Accounts is the blueprint of the accounts in your organization.\nIt is a tree view of the names of the Accounts (Ledgers and Groups) that a Company requires to manage its books of accounts. ERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to your needs and legal requirements.\n\nFor each company, Chart of Accounts signifies the way to classify the accounting entries, mostly\nbased on statutory (tax, compliance to government regulations) requirements.\n\nThere's a brief video tutorial about chart of accounts in the next step.",
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
@ -12,7 +12,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-30 14:35:59.474920",
"modified": "2021-08-13 11:46:25.878506",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",
@ -21,5 +21,6 @@
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Chart of Accounts",
"validate_action": 0
"validate_action": 0,
"video_url": "https://www.youtube.com/embed/AcfMCT7wLLo"
}

View File

@ -0,0 +1,22 @@
{
"action": "Go to Page",
"action_label": "Let's Review your Company",
"creation": "2021-06-29 14:47:42.497318",
"description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:43:35.767341",
"modified_by": "Administrator",
"name": "Company",
"owner": "Administrator",
"path": "app/company",
"reference_document": "Company",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Company",
"validate_action": 1
}

View File

@ -0,0 +1,21 @@
{
"action": "Go to Page",
"action_label": "View Cost Center Tree",
"creation": "2021-07-12 12:02:05.726608",
"description": "# Cost Centers for Budgeting and Analysis\n\nWhile your Books of Accounts are framed to fulfill statutory requirements, you can set up Cost Center and Accounting Dimensions to address your companies reporting and budgeting requirements.\n\nClick here to learn more about how <b>[Cost Center](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/cost-center)</b> and <b> [Dimensions](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-dimensions)</b> allow you to get advanced financial analytics reports from ERPNext.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:55:08.510366",
"modified_by": "Administrator",
"name": "Cost Centers for Report and Budgeting",
"owner": "Administrator",
"path": "cost-center/view/tree",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Cost Centers for Budgeting and Analysis",
"validate_action": 1
}

View File

@ -1,14 +1,15 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Let\u2019s create your first Purchase Invoice",
"creation": "2020-05-14 22:10:07.049704",
"description": "# What's a Purchase Invoice?\n\nA Purchase Invoice is a bill you receive from your Suppliers against which you need to make the payment.\nPurchase Invoice is the exact opposite of your Sales Invoice. Here you accrue expenses to your Supplier. \n\nThe following is what a typical purchase cycle looks like, however you can create a purchase invoice directly as well.\n\n![Purchase Flow](https://docs.erpnext.com/docs/assets/img/accounts/pi-flow.png)\n\n",
"description": "# Create your first Purchase Invoice\n\nA Purchase Invoice is a bill received from a Supplier for a product(s) or service(s) delivery to your company. You can track payables through Purchase Invoice and process Payment Entries against it.\n\nPurchase Invoices can also be created against a Purchase Order or Purchase Receipt.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-30 15:30:26.337773",
"modified": "2021-08-13 11:56:11.677253",
"modified_by": "Administrator",
"name": "Create Your First Purchase Invoice",
"owner": "Administrator",

View File

@ -0,0 +1,23 @@
{
"action": "View Report",
"creation": "2021-07-12 12:08:47.026115",
"description": "# Financial Statements\n\nIn ERPNext, you can get crucial financial reports like [Balance Sheet] and [Profit and Loss] statements with a click of a button. You can run in the report for a different period and plot analytics charts premised on statement data. For more reports, check sections like Financial Statements, General Ledger, and Profitability reports.\n\n<b>[Check Accounting reports](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-reports)</b>",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:59:18.767407",
"modified_by": "Administrator",
"name": "Financial Statements",
"owner": "Administrator",
"reference_report": "General Ledger",
"report_description": "General Ledger",
"report_reference_doctype": "GL Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Financial Statements",
"validate_action": 1
}

View File

@ -1,21 +1,21 @@
{
"action": "Create Entry",
"action_label": "Make a Sales Tax Template",
"action_label": "Manage Sales Tax Templates",
"creation": "2020-05-13 19:29:43.844463",
"description": "# Setting up Taxes\n\nAny sophisticated accounting system, including ERPNext will have automatic tax calculations for your transactions. These calculations are based on user defined rules in compliance to local rules and regulations.\n\nERPNext allows this via *Tax Templates*. These templates can be used in Sales Orders and Sales Invoices. Other types of charges that may apply to your invoices (like shipping, insurance etc.) can also be configured as taxes.\n\nFor Tax Accounts that you want to use in the tax templates, go to:\n\n`> Accounting > Taxes > Sales Taxes and Charges Template`\n\nYou can read more about these templates in our documentation [here](https://docs.erpnext.com/docs/user/manual/en/selling/sales-taxes-and-charges-template)\n",
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-30 14:54:18.087383",
"modified": "2021-08-13 11:48:37.238610",
"modified_by": "Administrator",
"name": "Setup Taxes",
"owner": "Administrator",
"reference_document": "Sales Taxes and Charges Template",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Lets create a Tax Template for Sales ",
"title": "Setting up Taxes",
"validate_action": 0
}

View File

@ -0,0 +1,22 @@
{
"action": "Watch Video",
"action_label": "Learn how to update opening balances",
"creation": "2021-07-12 11:53:50.525030",
"description": "# Updating Opening Balances\n\nOnce you close the financial statement in previous accounting software, you can update the same as opening in your ERPNext's Balance Sheet accounts. This will allow you to get complete financial statements from ERPNext in the coming years, and discontinue the parallel accounting system right away.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"intro_video_url": "https://www.youtube.com/embed/U5wPIvEn-0c",
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:56:45.483418",
"modified_by": "Administrator",
"name": "Updating Opening Balances",
"owner": "Administrator",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Updating Opening Balances",
"validate_action": 1,
"video_url": "https://www.youtube.com/embed/U5wPIvEn-0c"
}

View File

@ -341,31 +341,42 @@ def add_cc(args=None):
def reconcile_against_document(args):
"""
Cancel JV, Update aginst document, split if required and resubmit jv
Cancel PE or JV, Update against document, split if required and resubmit
"""
for d in args:
# To optimize making GL Entry for PE or JV with multiple references
reconciled_entries = {}
for row in args:
if not reconciled_entries.get((row.voucher_type, row.voucher_no)):
reconciled_entries[(row.voucher_type, row.voucher_no)] = []
check_if_advance_entry_modified(d)
validate_allocated_amount(d)
reconciled_entries[(row.voucher_type, row.voucher_no)].append(row)
for key, entries in reconciled_entries.items():
voucher_type = key[0]
voucher_no = key[1]
# cancel advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no)
doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True
doc.make_gl_entries(cancel=1, adv_adj=1)
# update ref in advance entry
if d.voucher_type == "Journal Entry":
update_reference_in_journal_entry(d, doc)
else:
update_reference_in_payment_entry(d, doc)
for entry in entries:
check_if_advance_entry_modified(entry)
validate_allocated_amount(entry)
# update ref in advance entry
if voucher_type == "Journal Entry":
update_reference_in_journal_entry(entry, doc, do_not_save=True)
else:
update_reference_in_payment_entry(entry, doc, do_not_save=True)
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no)
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
doc.make_gl_entries(cancel = 0, adv_adj =1)
frappe.flags.ignore_party_validation = False
if d.voucher_type in ('Payment Entry', 'Journal Entry'):
if entry.voucher_type in ('Payment Entry', 'Journal Entry'):
doc.update_expense_claim()
def check_if_advance_entry_modified(args):
@ -374,6 +385,9 @@ def check_if_advance_entry_modified(args):
check if amount is same
check if jv is submitted
"""
if not args.get('unreconciled_amount'):
args.update({'unreconciled_amount': args.get('unadjusted_amount')})
ret = None
if args.voucher_type == "Journal Entry":
ret = frappe.db.sql("""
@ -395,14 +409,14 @@ def check_if_advance_entry_modified(args):
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s
and t2.reference_doctype in ("", "Sales Order", "Purchase Order")
and t2.allocated_amount = %(unadjusted_amount)s
and t2.allocated_amount = %(unreconciled_amount)s
""".format(party_account_field), args)
else:
ret = frappe.db.sql("""select name from `tabPayment Entry`
where
name = %(voucher_no)s and docstatus = 1
and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s
and unallocated_amount = %(unadjusted_amount)s
and unallocated_amount = %(unreconciled_amount)s
""".format(party_account_field), args)
if not ret:
@ -415,58 +429,44 @@ def validate_allocated_amount(args):
elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision):
throw(_("Allocated amount cannot be greater than unadjusted amount"))
def update_reference_in_journal_entry(d, jv_obj):
def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
Updates against document, if partial amount splits into rows
"""
jv_detail = jv_obj.get("accounts", {"name": d["voucher_detail_no"]})[0]
jv_detail.set(d["dr_or_cr"], d["allocated_amount"])
jv_detail.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit',
d["allocated_amount"]*flt(jv_detail.exchange_rate))
original_reference_type = jv_detail.reference_type
original_reference_name = jv_detail.reference_name
jv_detail.set("reference_type", d["against_voucher_type"])
jv_detail.set("reference_name", d["against_voucher"])
if d['allocated_amount'] < d['unadjusted_amount']:
jvd = frappe.db.sql("""
select cost_center, balance, against_account, is_advance,
account_type, exchange_rate, account_currency
from `tabJournal Entry Account` where name = %s
""", d['voucher_detail_no'], as_dict=True)
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
if flt(d['unadjusted_amount']) - flt(d['allocated_amount']) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d['unadjusted_amount']) - flt(d['allocated_amount'])
amount_in_company_currency = amount_in_account_currency * flt(jvd[0]['exchange_rate'])
amount_in_company_currency = amount_in_account_currency * flt(jv_detail.exchange_rate)
jv_detail.set(d['dr_or_cr'], amount_in_account_currency)
jv_detail.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', amount_in_company_currency)
else:
journal_entry.remove(jv_detail)
# new entry with balance amount
ch = jv_obj.append("accounts")
ch.account = d['account']
ch.account_type = jvd[0]['account_type']
ch.account_currency = jvd[0]['account_currency']
ch.exchange_rate = jvd[0]['exchange_rate']
ch.party_type = d["party_type"]
ch.party = d["party"]
ch.cost_center = cstr(jvd[0]["cost_center"])
ch.balance = flt(jvd[0]["balance"])
# new row with references
new_row = journal_entry.append("accounts")
new_row.update(jv_detail.as_dict().copy())
ch.set(d['dr_or_cr'], amount_in_account_currency)
ch.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit', amount_in_company_currency)
new_row.set(d["dr_or_cr"], d["allocated_amount"])
new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',
d["allocated_amount"] * flt(jv_detail.exchange_rate))
ch.set('credit_in_account_currency' if d['dr_or_cr']== 'debit_in_account_currency'
else 'debit_in_account_currency', 0)
ch.set('credit' if d['dr_or_cr']== 'debit_in_account_currency' else 'debit', 0)
new_row.set('credit_in_account_currency' if d['dr_or_cr'] == 'debit_in_account_currency'
else 'debit_in_account_currency', 0)
new_row.set('credit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'debit', 0)
ch.against_account = cstr(jvd[0]["against_account"])
ch.reference_type = original_reference_type
ch.reference_name = original_reference_name
ch.is_advance = cstr(jvd[0]["is_advance"])
ch.docstatus = 1
new_row.set("reference_type", d["against_voucher_type"])
new_row.set("reference_name", d["against_voucher"])
new_row.against_account = cstr(jv_detail.against_account)
new_row.is_advance = cstr(jv_detail.is_advance)
new_row.docstatus = 1
# will work as update after submit
jv_obj.flags.ignore_validate_update_after_submit = True
jv_obj.save(ignore_permissions=True)
journal_entry.flags.ignore_validate_update_after_submit = True
if not do_not_save:
journal_entry.save(ignore_permissions=True)
def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
reference_details = {
@ -576,7 +576,7 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
@frappe.whitelist()
def get_company_default(company, fieldname, ignore_validation=False):
value = frappe.get_cached_value('Company', company, fieldname)
value = frappe.get_cached_value('Company', company, fieldname)
if not ignore_validation and not value:
throw(_("Please set default {0} in Company {1}")
@ -1086,3 +1086,14 @@ def get_journal_entry(account, stock_adjustment_account, amount):
db_or_cr_stock_adjustment_account : abs(amount)
}]
}
def check_and_delete_linked_reports(report):
""" Check if reports are referenced in Desktop Icon """
icons = frappe.get_all("Desktop Icon",
fields = ['name'],
filters = {
"_report": report
})
if icons:
for icon in icons:
frappe.delete_doc("Desktop Icon", icon)

View File

@ -233,6 +233,15 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
@ -340,6 +349,15 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Purchase Invoice",
"hidden": 0,
@ -1188,7 +1206,7 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:52.872470",
"modified": "2021-08-27 12:15:52.872470",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -1249,4 +1267,4 @@
}
],
"title": "Accounting"
}
}

View File

@ -0,0 +1,125 @@
{
"creation": "2021-08-24 16:55:10.923434",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-24 16:55:10.923434",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
"owner": "Administrator",
"reference_doctype": "Asset",
"save_on_complete": 0,
"steps": [
{
"description": "Select Naming Series based on which Asset ID will be generated",
"field": "",
"fieldname": "naming_series",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Naming Series",
"parent_field": "",
"position": "Bottom",
"title": "Naming Series"
},
{
"description": "Select an Asset Item",
"field": "",
"fieldname": "item_code",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Item Code",
"parent_field": "",
"position": "Bottom",
"title": "Item Code"
},
{
"description": "Select a Location",
"field": "",
"fieldname": "location",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Location",
"parent_field": "",
"position": "Bottom",
"title": "Location"
},
{
"description": "Check Is Existing Asset",
"field": "",
"fieldname": "is_existing_asset",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Is Existing Asset",
"parent_field": "",
"position": "Bottom",
"title": "Is Existing Asset?"
},
{
"description": "Set Available for use date",
"field": "",
"fieldname": "available_for_use_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Available-for-use Date",
"parent_field": "",
"position": "Bottom",
"title": "Available For Use Date"
},
{
"description": "Set Gross purchase amount",
"field": "",
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Gross Purchase Amount",
"parent_field": "",
"position": "Bottom",
"title": "Gross Purchase Amount"
},
{
"description": "Set Purchase Date",
"field": "",
"fieldname": "purchase_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Purchase Date",
"parent_field": "",
"position": "Bottom",
"title": "Purchase Date"
},
{
"description": "Check Calculate Depreciation",
"field": "",
"fieldname": "calculate_depreciation",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Calculate Depreciation",
"parent_field": "",
"position": "Bottom",
"title": "Calculate Depreciation"
},
{
"description": "Enter depreciation which has already been booked for this asset",
"field": "",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Opening Accumulated Depreciation",
"parent_field": "",
"position": "Bottom",
"title": "Accumulated Depreciation"
}
],
"title": "Asset"
}

View File

@ -0,0 +1,65 @@
{
"creation": "2021-08-24 12:48:20.763173",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-24 12:48:20.763173",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
"owner": "Administrator",
"reference_doctype": "Asset Category",
"save_on_complete": 0,
"steps": [
{
"description": "Name Asset category. You can create categories based on Asset Types like Furniture, Property, Electronics etc.",
"field": "",
"fieldname": "asset_category_name",
"fieldtype": "Data",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Asset Category Name",
"parent_field": "",
"position": "Bottom",
"title": "Asset Category Name"
},
{
"description": "Check to enable Capital Work in Progress accounting",
"field": "",
"fieldname": "enable_cwip_accounting",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Enable Capital Work in Progress Accounting",
"parent_field": "",
"position": "Bottom",
"title": "Enable CWIP Accounting"
},
{
"description": "Add a row to define Depreciation Method and other details. Note that you can leave Finance Book blank to have it's accounting done in the primary books of accounts.",
"field": "",
"fieldname": "finance_books",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Finance Books",
"parent_field": "",
"position": "Bottom",
"title": "Finance Book Detail"
},
{
"description": "Select the Fixed Asset and Depreciation accounts applicable for this Asset Category type",
"field": "",
"fieldname": "accounts",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Accounts",
"parent_field": "",
"position": "Bottom",
"title": "Accounts"
}
],
"title": "Asset Category"
}

View File

@ -13,26 +13,26 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset",
"idx": 0,
"is_complete": 0,
"modified": "2020-07-08 14:05:51.828497",
"modified": "2021-08-24 17:50:41.573281",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
"owner": "Administrator",
"steps": [
{
"step": "Introduction to Assets"
"step": "Fixed Asset Accounts"
},
{
"step": "Create a Fixed Asset Item"
"step": "Asset Category"
},
{
"step": "Create an Asset Category"
"step": "Asset Item"
},
{
"step": "Purchase an Asset Item"
"step": "Asset Purchase"
},
{
"step": "Create an Asset"
"step": "Existing Asset"
}
],
"subtitle": "Assets, Depreciations, Repairs, and more.",

View File

@ -0,0 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Let's review existing Asset Category",
"creation": "2021-08-13 14:26:18.656303",
"description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-24 12:49:37.665239",
"modified_by": "Administrator",
"name": "Asset Category",
"owner": "Administrator",
"reference_document": "Asset Category",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Define Asset Category",
"validate_action": 1
}

View File

@ -0,0 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Let's create a new Asset item",
"creation": "2021-08-13 14:27:07.277167",
"description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-16 13:59:18.362233",
"modified_by": "Administrator",
"name": "Asset Item",
"owner": "Administrator",
"reference_document": "Item",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create an Asset Item",
"validate_action": 1
}

View File

@ -0,0 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Let's create a Purchase Receipt",
"creation": "2021-08-13 14:27:53.678621",
"description": "# Purchase an Asset\n\nAssets purchases process if done following the standard Purchase cycle. If capital work in progress is enabled in Asset Category, Asset will be created as soon as Purchase Receipt is created for it. You can quickly create a Purchase Receipt for Asset and see its impact on books of accounts.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-24 17:26:57.180637",
"modified_by": "Administrator",
"name": "Asset Purchase",
"owner": "Administrator",
"reference_document": "Purchase Receipt",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Purchase an Asset",
"validate_action": 1
}

View File

@ -0,0 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Add an existing Asset",
"creation": "2021-08-13 14:28:30.650459",
"description": "# Add an Existing Asset\n\nIf you are just starting with ERPNext, you will need to enter Assets you already possess. You can add them as existing fixed assets in ERPNext. Please note that you will have to make a Journal Entry separately updating the opening balance in the fixed asset account.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-16 14:03:48.850471",
"modified_by": "Administrator",
"name": "Existing Asset",
"owner": "Administrator",
"reference_document": "Asset",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Add an Existing Asset",
"validate_action": 1
}

View File

@ -0,0 +1,21 @@
{
"action": "Go to Page",
"action_label": "Let's walk-through Chart of Accounts to review setup",
"creation": "2021-08-13 14:23:09.297765",
"description": "# Fixed Asset Accounts\n\nWith the company, a host of fixed asset accounts are pre-configured. To ensure your asset transactions are leading to correct accounting entries, you can review and set up following asset accounts as per your business requirements.\n - Fixed asset accounts (Asset account)\n - Accumulated depreciation\n - Capital Work in progress (CWIP) account\n - Asset Depreciation account (Expense account)",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-24 17:46:37.646174",
"modified_by": "Administrator",
"name": "Fixed Asset Accounts",
"owner": "Administrator",
"path": "app/account/view/tree",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Fixed Asset Accounts",
"validate_action": 1
}

View File

@ -24,7 +24,26 @@ frappe.ui.form.on("Supplier", {
}
}
});
frm.set_query("supplier_primary_contact", function(doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
filters: {
"supplier": doc.name
}
};
});
frm.set_query("supplier_primary_address", function(doc) {
return {
filters: {
"link_doctype": "Supplier",
"link_name": doc.name
}
};
});
},
refresh: function (frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' }
@ -78,6 +97,30 @@ frappe.ui.form.on("Supplier", {
});
},
supplier_primary_address: function(frm) {
if (frm.doc.supplier_primary_address) {
frappe.call({
method: 'frappe.contacts.doctype.address.address.get_address_display',
args: {
"address_dict": frm.doc.supplier_primary_address
},
callback: function(r) {
frm.set_value("primary_address", r.message);
}
});
}
if (!frm.doc.supplier_primary_address) {
frm.set_value("primary_address", "");
}
},
supplier_primary_contact: function(frm) {
if (!frm.doc.supplier_primary_contact) {
frm.set_value("mobile_no", "");
frm.set_value("email_id", "");
}
},
is_internal_supplier: function(frm) {
if (frm.doc.is_internal_supplier == 1) {
frm.toggle_reqd("represents_company", true);

View File

@ -49,6 +49,13 @@
"address_html",
"column_break1",
"contact_html",
"primary_address_and_contact_detail_section",
"supplier_primary_contact",
"mobile_no",
"email_id",
"column_break_44",
"supplier_primary_address",
"primary_address",
"default_payable_accounts",
"accounts",
"default_tax_withholding_config",
@ -378,6 +385,47 @@
"fieldname": "allow_purchase_invoice_creation_without_purchase_receipt",
"fieldtype": "Check",
"label": "Allow Purchase Invoice Creation Without Purchase Receipt"
},
{
"fieldname": "primary_address_and_contact_detail_section",
"fieldtype": "Section Break",
"label": "Primary Address and Contact Detail"
},
{
"description": "Reselect, if the chosen contact is edited after save",
"fieldname": "supplier_primary_contact",
"fieldtype": "Link",
"label": "Supplier Primary Contact",
"options": "Contact"
},
{
"fetch_from": "supplier_primary_contact.mobile_no",
"fieldname": "mobile_no",
"fieldtype": "Read Only",
"label": "Mobile No"
},
{
"fetch_from": "supplier_primary_contact.email_id",
"fieldname": "email_id",
"fieldtype": "Read Only",
"label": "Email Id"
},
{
"fieldname": "column_break_44",
"fieldtype": "Column Break"
},
{
"fieldname": "primary_address",
"fieldtype": "Text",
"label": "Primary Address",
"read_only": 1
},
{
"description": "Reselect, if the chosen address is edited after save",
"fieldname": "supplier_primary_address",
"fieldtype": "Link",
"label": "Supplier Primary Address",
"options": "Address"
}
],
"icon": "fa fa-user",
@ -390,7 +438,7 @@
"link_fieldname": "supplier"
}
],
"modified": "2021-05-18 15:10:11.087191",
"modified": "2021-08-27 18:02:44.314077",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@ -42,7 +42,12 @@ class Supplier(TransactionBase):
if not self.naming_series:
self.naming_series = ''
self.create_primary_contact()
self.create_primary_address()
def validate(self):
self.flags.is_new_doc = self.is_new()
# validation for Naming Series mandatory field...
if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series':
if not self.naming_series:
@ -76,7 +81,39 @@ class Supplier(TransactionBase):
frappe.throw(_("Internal Supplier for company {0} already exists").format(
frappe.bold(self.represents_company)))
def create_primary_contact(self):
from erpnext.selling.doctype.customer.customer import make_contact
if not self.supplier_primary_contact:
if self.mobile_no or self.email_id:
contact = make_contact(self)
self.db_set('supplier_primary_contact', contact.name)
self.db_set('mobile_no', self.mobile_no)
self.db_set('email_id', self.email_id)
def create_primary_address(self):
from erpnext.selling.doctype.customer.customer import make_address
from frappe.contacts.doctype.address.address import get_address_display
if self.flags.is_new_doc and self.get('address_line1'):
address = make_address(self)
address_display = get_address_display(address.name)
self.db_set("supplier_primary_address", address.name)
self.db_set("primary_address", address_display)
def on_trash(self):
if self.supplier_primary_contact:
frappe.db.sql("""
UPDATE `tabSupplier`
SET
supplier_primary_contact=null,
supplier_primary_address=null,
mobile_no=null,
email_id=null,
primary_address=null
WHERE name=%(name)s""", {"name": self.name})
delete_contact_and_address('Supplier', self.name)
def after_rename(self, olddn, newdn, merge=False):
@ -104,3 +141,21 @@ class Supplier(TransactionBase):
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
pass
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
return frappe.db.sql("""
SELECT
`tabContact`.name from `tabContact`,
`tabDynamic Link`
WHERE
`tabContact`.name = `tabDynamic Link`.parent
and `tabDynamic Link`.link_name = %(supplier)s
and `tabDynamic Link`.link_doctype = 'Supplier'
and `tabContact`.name like %(txt)s
""", {
'supplier': supplier,
'txt': '%%%s%%' % txt
})

View File

@ -1206,7 +1206,7 @@ class AccountsController(TransactionBase):
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
d.base_payment_amount = flt(d.payment_amount * self.get("conversion_rate"), d.precision('base_payment_amount'))
def get_order_details(self):
@ -1587,7 +1587,7 @@ def get_advance_journal_entries(party_type, party, party_account, amount_field,
def get_advance_payment_entries(party_type, party, party_account, order_doctype,
order_list=None, include_unallocated=True, against_all_orders=False, limit=None):
order_list=None, include_unallocated=True, against_all_orders=False, limit=None, condition=None):
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
payment_type = "Receive" if party_type == "Customer" else "Pay"
@ -1622,14 +1622,14 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
if include_unallocated:
unallocated_payment_entries = frappe.db.sql("""
select "Payment Entry" as reference_type, name as reference_name,
remarks, unallocated_amount as amount, {2} as exchange_rate
select "Payment Entry" as reference_type, name as reference_name, posting_date,
remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
from `tabPayment Entry`
where
{0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0
and docstatus = 1 and unallocated_amount > 0 {condition}
order by posting_date {1}
""".format(party_account_field, limit_cond, exchange_rate_field),
""".format(party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""),
(party_account, party_type, party, payment_type), as_dict=1)
return list(payment_entries_against_order) + list(unallocated_payment_entries)

View File

@ -39,6 +39,8 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
}
if (!this.frm.is_new()) {
@ -49,27 +51,74 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
}
make_customer () {
add_lead_to_prospect (frm) {
frappe.prompt([
{
fieldname: 'prospect',
label: __('Prospect'),
fieldtype: 'Link',
options: 'Prospect',
reqd: 1
}
],
function(data) {
frappe.call({
method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect',
args: {
'lead': frm.doc.name,
'prospect': data.prospect
},
callback: function(r) {
if (!r.exc) {
frm.reload_doc();
}
},
freeze: true,
freeze_message: __('...Adding Lead to Prospect')
});
}, __('Add Lead to Prospect'), __('Add'));
}
make_customer (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_customer",
frm: cur_frm
frm: frm
})
}
make_opportunity () {
make_opportunity (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: cur_frm
frm: frm
})
}
make_quotation () {
make_quotation (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_quotation",
frm: cur_frm
frm: frm
})
}
make_prospect (frm) {
frappe.model.with_doctype("Prospect", function() {
let prospect = frappe.model.get_new_doc("Prospect");
prospect.company_name = frm.doc.company_name;
prospect.no_of_employees = frm.doc.no_of_employees;
prospect.industry = frm.doc.industry;
prospect.market_segment = frm.doc.market_segment;
prospect.territory = frm.doc.territory;
prospect.fax = frm.doc.fax;
prospect.website = frm.doc.website;
prospect.prospect_owner = frm.doc.lead_owner;
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
lead_prospect_row.lead = frm.doc.name;
frappe.set_route("Form", "Prospect", prospect.name);
});
}
company_name () {
if (!this.frm.doc.lead_name) {
this.frm.set_value("lead_name", this.frm.doc.company_name);

View File

@ -63,6 +63,7 @@ class Lead(SellingController):
def on_update(self):
self.add_calendar_event()
self.update_prospects()
def before_insert(self):
self.contact_doc = self.create_contact()
@ -89,6 +90,12 @@ class Lead(SellingController):
"description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
}, force)
def update_prospects(self):
prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
for row in prospects:
prospect = frappe.get_doc('Prospect', row.parent)
prospect.save(ignore_permissions=True)
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
@ -354,3 +361,14 @@ def daily_open_lead():
leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]])
for lead in leads:
frappe.db.set_value("Lead", lead.name, "status", "Open")
@frappe.whitelist()
def add_lead_to_prospect(lead, prospect):
prospect = frappe.get_doc('Prospect', prospect)
prospect.append('prospect_lead', {
'lead': lead
})
prospect.save(ignore_permissions=True)
frappe.msgprint(_('Lead {0} has been added to prospect {1}.').format(frappe.bold(lead), frappe.bold(prospect.name)),
title=_('Lead Added'), indicator='green')

View File

@ -13,7 +13,7 @@ def get_data():
},
'transactions': [
{
'items': ['Opportunity', 'Quotation']
'items': ['Opportunity', 'Quotation', 'Prospect']
},
]
}

View File

@ -0,0 +1,28 @@
frappe.listview_settings['Lead'] = {
onload: function(listview) {
if (frappe.boot.user.can_create.includes("Prospect")) {
listview.page.add_action_item(__("Create Prospect"), function() {
frappe.model.with_doctype("Prospect", function() {
let prospect = frappe.model.get_new_doc("Prospect");
let leads = listview.get_checked_items();
frappe.db.get_value("Lead", leads[0].name, ["company_name", "no_of_employees", "industry", "market_segment", "territory", "fax", "website", "lead_owner"], (r) => {
prospect.company_name = r.company_name;
prospect.no_of_employees = r.no_of_employees;
prospect.industry = r.industry;
prospect.market_segment = r.market_segment;
prospect.territory = r.territory;
prospect.fax = r.fax;
prospect.website = r.website;
prospect.prospect_owner = r.lead_owner;
leads.forEach(function(lead) {
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
lead_prospect_row.lead = lead.name;
});
frappe.set_route("Form", "Prospect", prospect.name);
});
});
});
}
}
};

View File

@ -10,12 +10,12 @@ frappe.ui.form.on("Opportunity", {
frm.custom_make_buttons = {
'Quotation': 'Quotation',
'Supplier Quotation': 'Supplier Quotation'
},
};
frm.set_query("opportunity_from", function() {
return{
"filters": {
"name": ["in", ["Customer", "Lead"]],
"name": ["in", ["Customer", "Lead", "Prospect"]],
}
}
});

View File

@ -430,7 +430,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"modified": "2021-06-04 10:11:22.831139",
"modified": "2021-08-25 10:28:24.923543",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

View File

@ -0,0 +1,29 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Prospect', {
refresh (frm) {
if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
frm.add_custom_button(__("Customer"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.prospect.prospect.make_customer",
frm: frm
});
}, __("Create"));
}
if (!frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) {
frm.add_custom_button(__("Opportunity"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.prospect.prospect.make_opportunity",
frm: frm
});
}, __("Create"));
}
if (!frm.is_new()) {
frappe.contacts.render_address_and_contact(frm);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
}
});

View File

@ -0,0 +1,203 @@
{
"actions": [],
"autoname": "field:company_name",
"creation": "2021-08-19 00:21:06.995448",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company_name",
"industry",
"market_segment",
"customer_group",
"territory",
"column_break_6",
"no_of_employees",
"currency",
"annual_revenue",
"more_details_section",
"fax",
"website",
"column_break_13",
"prospect_owner",
"leads_section",
"prospect_lead",
"address_and_contact_section",
"address_html",
"column_break_17",
"contact_html",
"notes_section",
"notes"
],
"fields": [
{
"fieldname": "company_name",
"fieldtype": "Data",
"label": "Company Name",
"unique": 1
},
{
"fieldname": "industry",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Industry",
"options": "Industry Type"
},
{
"fieldname": "market_segment",
"fieldtype": "Link",
"label": "Market Segment",
"options": "Market Segment"
},
{
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"options": "Customer Group"
},
{
"fieldname": "territory",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Territory",
"options": "Territory"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "no_of_employees",
"fieldtype": "Int",
"label": "No. of Employees"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "annual_revenue",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Annual Revenue",
"options": "currency"
},
{
"fieldname": "fax",
"fieldtype": "Data",
"label": "Fax",
"options": "Phone"
},
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"options": "URL"
},
{
"fieldname": "prospect_owner",
"fieldtype": "Link",
"label": "Prospect Owner",
"options": "User"
},
{
"fieldname": "leads_section",
"fieldtype": "Section Break",
"label": "Leads"
},
{
"fieldname": "prospect_lead",
"fieldtype": "Table",
"options": "Prospect Lead"
},
{
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML"
},
{
"collapsible": 1,
"fieldname": "notes_section",
"fieldtype": "Section Break",
"label": "Notes"
},
{
"fieldname": "notes",
"fieldtype": "Text Editor"
},
{
"fieldname": "more_details_section",
"fieldtype": "Section Break",
"label": "More Details"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: !doc.__islocal",
"fieldname": "address_and_contact_section",
"fieldtype": "Section Break",
"label": "Address and Contact"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-27 16:24:42.961967",
"modified_by": "Administrator",
"module": "CRM",
"name": "Prospect",
"owner": "Administrator",
"permissions": [
{
"create": 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,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company_name",
"track_changes": 1
}

View File

@ -0,0 +1,99 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.contacts.address_and_contact import load_address_and_contact
class Prospect(Document):
def onload(self):
load_address_and_contact(self)
def validate(self):
self.update_lead_details()
def on_update(self):
self.link_with_lead_contact_and_address()
def on_trash(self):
self.unlink_dynamic_links()
def update_lead_details(self):
for row in self.get('prospect_lead'):
lead = frappe.get_value('Lead', row.lead, ['lead_name', 'status', 'email_id', 'mobile_no'], as_dict=True)
row.lead_name = lead.lead_name
row.status = lead.status
row.email = lead.email_id
row.mobile_no = lead.mobile_no
def link_with_lead_contact_and_address(self):
for row in self.prospect_lead:
links = frappe.get_all('Dynamic Link', filters={'link_doctype': 'Lead', 'link_name': row.lead}, fields=['parent', 'parenttype'])
for link in links:
linked_doc = frappe.get_doc(link['parenttype'], link['parent'])
exists = False
for d in linked_doc.get('links'):
if d.link_doctype == self.doctype and d.link_name == self.name:
exists = True
if not exists:
linked_doc.append('links', {
'link_doctype': self.doctype,
'link_name': self.name
})
linked_doc.save(ignore_permissions=True)
def unlink_dynamic_links(self):
links = frappe.get_all('Dynamic Link', filters={'link_doctype': self.doctype, 'link_name': self.name}, fields=['parent', 'parenttype'])
for link in links:
linked_doc = frappe.get_doc(link['parenttype'], link['parent'])
if len(linked_doc.get('links')) == 1:
linked_doc.delete(ignore_permissions=True)
else:
to_remove = None
for d in linked_doc.get('links'):
if d.link_doctype == self.doctype and d.link_name == self.name:
to_remove = d
if to_remove:
linked_doc.remove(to_remove)
linked_doc.save(ignore_permissions=True)
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
def set_missing_values(source, target):
target.customer_type = "Company"
target.company_name = source.name
target.customer_group = source.customer_group or frappe.db.get_default("Customer Group")
doclist = get_mapped_doc("Prospect", source_name,
{"Prospect": {
"doctype": "Customer",
"field_map": {
"company_name": "customer_name",
"currency": "default_currency",
"fax": "fax"
}
}}, target_doc, set_missing_values, ignore_permissions=False)
return doclist
@frappe.whitelist()
def make_opportunity(source_name, target_doc=None):
def set_missing_values(source, target):
target.opportunity_from = "Prospect"
target.customer_name = source.company_name
target.customer_group = source.customer_group or frappe.db.get_default("Customer Group")
doclist = get_mapped_doc("Prospect", source_name,
{"Prospect": {
"doctype": "Opportunity",
"field_map": {
"name": "party_name",
}
}}, target_doc, set_missing_values, ignore_permissions=False)
return doclist

View File

@ -0,0 +1,54 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.utils import random_string
from erpnext.crm.doctype.lead.test_lead import make_lead
from erpnext.crm.doctype.lead.lead import add_lead_to_prospect
class TestProspect(unittest.TestCase):
def test_add_lead_to_prospect_and_address_linking(self):
lead_doc = make_lead()
address_doc = make_address(address_title=lead_doc.name)
address_doc.append('links', {
"link_doctype": lead_doc.doctype,
"link_name": lead_doc.name
})
address_doc.save()
prospect_doc = make_prospect()
add_lead_to_prospect(lead_doc.name, prospect_doc.name)
prospect_doc.reload()
lead_exists_in_prosoect = False
for rec in prospect_doc.get('prospect_lead'):
if rec.lead == lead_doc.name:
lead_exists_in_prosoect = True
self.assertEqual(lead_exists_in_prosoect, True)
address_doc.reload()
self.assertEqual(address_doc.has_link('Prospect', prospect_doc.name), True)
def make_prospect(**args):
args = frappe._dict(args)
prospect_doc = frappe.get_doc({
"doctype": "Prospect",
"company_name": args.company_name or "_Test Company {}".format(random_string(3)),
}).insert()
return prospect_doc
def make_address(**args):
args = frappe._dict(args)
address_doc = frappe.get_doc({
"doctype": "Address",
"address_title": args.address_title or "Address Title",
"address_type": args.address_type or "Billing",
"city": args.city or "Mumbai",
"address_line1": args.address_line1 or "Vidya Vihar West",
"country": args.country or "India"
}).insert()
return address_doc

View File

@ -0,0 +1,67 @@
{
"actions": [],
"creation": "2021-08-19 00:14:14.857421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lead",
"lead_name",
"status",
"email",
"mobile_no"
],
"fields": [
{
"fieldname": "lead",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lead",
"options": "Lead",
"reqd": 1
},
{
"fieldname": "lead_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Lead Name",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
"read_only": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email",
"options": "Email",
"read_only": 1
},
{
"fieldname": "mobile_no",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Mobile No",
"options": "Phone",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-25 12:58:24.638054",
"modified_by": "Administrator",
"module": "CRM",
"name": "Prospect Lead",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProspectLead(Document):
pass

View File

@ -1,11 +1,10 @@
import traceback
import taxjar
import frappe
import taxjar
from erpnext import get_default_company
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
from frappe.utils import cint
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
@ -14,6 +13,10 @@ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_cal
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL',
'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE',
'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD',
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
def get_client():
@ -27,7 +30,11 @@ def get_client():
api_url = taxjar.SANDBOX_API_URL
if api_key and api_url:
return taxjar.Client(api_key=api_key, api_url=api_url)
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
'x-api-version': '2020-08-07'
})
return client
def create_transaction(doc, method):
@ -57,7 +64,10 @@ def create_transaction(doc, method):
tax_dict['amount'] = doc.total + tax_dict['shipping']
try:
client.create_order(tax_dict)
if doc.is_return:
client.create_refund(tax_dict)
else:
client.create_order(tax_dict)
except taxjar.exceptions.TaxJarResponseError as err:
frappe.throw(_(sanitize_error_response(err)))
except Exception as ex:
@ -89,14 +99,16 @@ def get_tax_data(doc):
to_country_code = frappe.db.get_value("Country", to_address.country, "code")
to_country_code = to_country_code.upper()
if to_country_code not in SUPPORTED_COUNTRY_CODES:
return
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
if to_shipping_state is not None:
to_shipping_state = get_iso_3166_2_state_code(to_address)
line_items = [get_line_item_dict(item) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
if to_shipping_state not in SUPPORTED_STATE_CODES:
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
'from_country': from_country_code,
'from_zip': from_address.pincode,
@ -109,11 +121,29 @@ def get_tax_data(doc):
'to_street': to_address.address_line1,
'to_state': to_shipping_state,
'shipping': shipping,
'amount': doc.net_total
'amount': doc.net_total,
'plugin': 'erpnext',
'line_items': line_items
}
return tax_dict
return tax_dict
def get_state_code(address, location):
if address is not None:
state_code = get_iso_3166_2_state_code(address)
if state_code not in SUPPORTED_STATE_CODES:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
else:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
return state_code
def get_line_item_dict(item):
return dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
@ -122,17 +152,7 @@ def set_sales_tax(doc, method):
if not doc.items:
return
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
if check_sales_tax_exemption(doc):
return
tax_dict = get_tax_data(doc)
@ -143,7 +163,6 @@ def set_sales_tax(doc, method):
return
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
@ -163,9 +182,28 @@ def set_sales_tax(doc, method):
"account_head": TAX_ACCOUNT_HEAD,
"tax_amount": tax_data.amount_to_collect
})
# Assigning values to tax_collectable and taxable_amount fields in sales item table
for item in tax_data.breakdown.line_items:
doc.get('items')[cint(item.id)-1].tax_collectable = item.tax_collectable
doc.get('items')[cint(item.id)-1].taxable_amount = item.taxable_amount
doc.run_method("calculate_taxes_and_totals")
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
return True
else:
return False
def validate_tax_request(tax_dict):
"""Return the sales tax that should be collected for a given order."""
@ -200,6 +238,8 @@ def get_shipping_address_details(doc):
if doc.shipping_address_name:
shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
elif doc.customer_address:
shipping_address = frappe.get_doc("Address", doc.customer_address_name)
else:
shipping_address = get_company_address_details(doc)

View File

@ -12,15 +12,15 @@
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-22 13:22:47.008622",
"modified": "2020-07-22 13:36:48.114479",
"last_synced_on": "2021-01-30 21:03:30.086891",
"modified": "2021-02-01 13:36:04.469863",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Clinical Procedures",
"number_of_groups": 0,
"owner": "Administrator",
"timeseries": 0,
"type": "Percentage",
"type": "Bar",
"use_report_chart": 0,
"y_axis": []
}

View File

@ -12,15 +12,15 @@
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-22 13:22:46.691764",
"modified": "2020-07-22 13:40:17.215775",
"last_synced_on": "2021-02-01 13:36:38.787783",
"modified": "2021-02-01 13:37:18.718275",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Clinical Procedures Status",
"number_of_groups": 0,
"owner": "Administrator",
"timeseries": 0,
"type": "Pie",
"type": "Bar",
"use_report_chart": 0,
"y_axis": []
}

View File

@ -5,21 +5,22 @@
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "Patient Encounter Diagnosis",
"dynamic_filters_json": "",
"filters_json": "[]",
"group_by_based_on": "diagnosis",
"group_by_type": "Count",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-22 13:22:47.895521",
"modified": "2020-07-22 13:43:32.369481",
"last_synced_on": "2021-01-30 21:03:33.729487",
"modified": "2021-02-01 13:34:57.385335",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Diagnoses",
"number_of_groups": 0,
"owner": "Administrator",
"timeseries": 0,
"type": "Percentage",
"type": "Bar",
"use_report_chart": 0,
"y_axis": []
}

View File

@ -12,15 +12,15 @@
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-22 13:22:47.344055",
"modified": "2020-07-22 13:37:34.490129",
"last_synced_on": "2021-01-30 21:03:28.272914",
"modified": "2021-02-01 13:36:08.391433",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Lab Tests",
"number_of_groups": 0,
"owner": "Administrator",
"timeseries": 0,
"type": "Percentage",
"type": "Bar",
"use_report_chart": 0,
"y_axis": []
}

View File

@ -12,15 +12,15 @@
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-22 13:22:47.296748",
"modified": "2020-07-22 13:40:59.655129",
"last_synced_on": "2021-01-30 21:03:32.067473",
"modified": "2021-02-01 13:35:30.953718",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Symptoms",
"number_of_groups": 0,
"owner": "Administrator",
"timeseries": 0,
"type": "Percentage",
"type": "Bar",
"use_report_chart": 0,
"y_axis": []
}

View File

@ -11,7 +11,7 @@ test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
self.assertTrue(frappe.db.exists('Item', procedure_template.item))
@ -20,7 +20,7 @@ class TestClinicalProcedure(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1)
def test_consumables(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
procedure_template.allow_stock_consumption = 1
consumable = create_consumable()

View File

@ -27,7 +27,7 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.automate_appointment_invoicing = 1
healthcare_settings.op_consulting_charge_item = item
healthcare_settings.save(ignore_permissions=True)
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
# For first appointment, invoice is generated. First appointment not considered in fee validity
appointment = create_appointment(patient, practitioner, nowdate())

View File

@ -7,8 +7,8 @@ frappe.ui.form.on('Healthcare Service Unit', {
// get query select healthcare service unit
frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) {
return{
filters:[
return {
filters: [
['Healthcare Service Unit', 'is_group', '=', 1],
['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name]
]
@ -21,6 +21,14 @@ frappe.ui.form.on('Healthcare Service Unit', {
frm.add_custom_button(__('Healthcare Service Unit Tree'), function() {
frappe.set_route('Tree', 'Healthcare Service Unit');
});
frm.set_query('warehouse', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
},
set_root_readonly: function(frm) {
// read-only for root healthcare service unit
@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', {
else {
frm.set_df_property('service_unit_type', 'reqd', 1);
}
},
overlap_appointments: function(frm) {
if (frm.doc.overlap_appointments == 0) {
frm.set_value('service_unit_capacity', '');
}
}
});

View File

@ -16,6 +16,7 @@
"service_unit_type",
"allow_appointments",
"overlap_appointments",
"service_unit_capacity",
"inpatient_occupancy",
"occupancy_status",
"column_break_9",
@ -31,6 +32,8 @@
{
"fieldname": "healthcare_service_unit_name",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
"in_list_view": 1,
"label": "Service Unit",
@ -41,6 +44,8 @@
"bold": 1,
"fieldname": "parent_healthcare_service_unit",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Parent Service Unit",
@ -52,6 +57,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1",
"fieldname": "is_group",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Group"
},
{
@ -59,6 +66,8 @@
"depends_on": "eval:doc.is_group != 1",
"fieldname": "service_unit_type",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Service Unit Type",
"options": "Healthcare Service Unit Type"
},
@ -68,6 +77,8 @@
"fetch_from": "service_unit_type.allow_appointments",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Allow Appointments",
"no_copy": 1,
@ -79,6 +90,8 @@
"fetch_from": "service_unit_type.overlap_appointments",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap",
"no_copy": 1,
"read_only": 1
@ -90,6 +103,8 @@
"fetch_from": "service_unit_type.inpatient_occupancy",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Inpatient Occupancy",
"no_copy": 1,
@ -100,6 +115,8 @@
"depends_on": "eval:doc.inpatient_occupancy == 1",
"fieldname": "occupancy_status",
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
"label": "Occupancy Status",
"no_copy": 1,
"options": "Vacant\nOccupied",
@ -107,13 +124,17 @@
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"bold": 1,
"depends_on": "eval:doc.is_group != 1",
"fieldname": "warehouse",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Warehouse",
"no_copy": 1,
"options": "Warehouse"
@ -121,6 +142,8 @@
{
"fieldname": "company",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"in_standard_filter": 1,
@ -134,6 +157,8 @@
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "lft",
"no_copy": 1,
"print_hide": 1,
@ -143,6 +168,8 @@
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "rgt",
"no_copy": 1,
"print_hide": 1,
@ -152,6 +179,8 @@
"fieldname": "old_parent",
"fieldtype": "Link",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"label": "Old Parent",
"no_copy": 1,
@ -163,14 +192,26 @@
"collapsible": 1,
"fieldname": "tree_details_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Tree Details"
},
{
"depends_on": "eval:doc.overlap_appointments == 1",
"fieldname": "service_unit_capacity",
"fieldtype": "Int",
"label": "Service Unit Capacity",
"mandatory_depends_on": "eval:doc.overlap_appointments == 1",
"non_negative": 1
}
],
"is_tree": 1,
"links": [],
"modified": "2020-05-20 18:26:56.065543",
"modified": "2021-08-19 14:09:11.643464",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit",
"nsm_parent_field": "parent_healthcare_service_unit",
"owner": "Administrator",
"permissions": [
{

View File

@ -5,14 +5,21 @@
from __future__ import unicode_literals
from frappe.utils.nestedset import NestedSet
from frappe.utils import cint, cstr
import frappe
from frappe import _
import json
class HealthcareServiceUnit(NestedSet):
nsm_parent_field = 'parent_healthcare_service_unit'
def validate(self):
self.set_service_unit_properties()
def autoname(self):
if self.company:
suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr")
suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr')
if not self.healthcare_service_unit_name.endswith(suffix):
self.name = self.healthcare_service_unit_name + suffix
else:
@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet):
super(HealthcareServiceUnit, self).on_update()
self.validate_one_root()
def after_insert(self):
if self.is_group:
self.allow_appointments = 0
self.overlap_appointments = 0
self.inpatient_occupancy = 0
elif self.service_unit_type:
def set_service_unit_properties(self):
if cint(self.is_group):
self.allow_appointments = False
self.overlap_appointments = False
self.inpatient_occupancy = False
self.service_unit_capacity = 0
self.occupancy_status = ''
self.service_unit_type = ''
elif self.service_unit_type != '':
service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type)
self.allow_appointments = service_unit_type.allow_appointments
self.overlap_appointments = service_unit_type.overlap_appointments
self.inpatient_occupancy = service_unit_type.inpatient_occupancy
if self.inpatient_occupancy:
if self.inpatient_occupancy and self.occupancy_status != '':
self.occupancy_status = 'Vacant'
self.overlap_appointments = 0
if service_unit_type.overlap_appointments:
self.overlap_appointments = True
else:
self.overlap_appointments = False
self.service_unit_capacity = 0
if self.overlap_appointments:
if not self.service_unit_capacity:
frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'),
title=_('Mandatory'))
@frappe.whitelist()
def add_multiple_service_units(parent, data):
'''
parent - parent service unit under which the service units are to be created
data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity
'''
if not parent or not data:
return
data = json.loads(data)
company = data.get('company') or \
frappe.defaults.get_defaults().get('company') or \
frappe.db.get_single_value('Global Defaults', 'default_company')
if not data.get('healthcare_service_unit_name') or not company:
frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'),
title=_('Missing Required Fields'))
count = cint(data.get('count') or 0)
if count <= 0:
frappe.throw(_('Number of Service Units to be created should at least be 1'),
title=_('Invalid Number of Service Units'))
capacity = cint(data.get('service_unit_capacity') or 1)
service_unit = {
'doctype': 'Healthcare Service Unit',
'parent_healthcare_service_unit': parent,
'service_unit_type': data.get('service_unit_type') or None,
'service_unit_capacity': capacity if capacity > 0 else 1,
'warehouse': data.get('warehouse') or None,
'company': company
}
service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -'))
last_suffix = frappe.db.sql("""SELECT
IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0)
FROM `tabHealthcare Service Unit`
WHERE name like %(prefix)s AND company=%(company)s""",
{'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company},
as_list=1)[0][0]
start_suffix = cint(last_suffix) + 1
failed_list = []
for i in range(start_suffix, count + start_suffix):
# name to be in the form WARD-####
service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i)))
service_unit_doc = frappe.get_doc(service_unit)
try:
service_unit_doc.insert()
except Exception:
failed_list.append(service_unit['healthcare_service_unit_name'])
return failed_list

View File

@ -1,35 +1,185 @@
frappe.treeview_settings["Healthcare Service Unit"] = {
breadcrumbs: "Healthcare Service Unit",
title: __("Healthcare Service Unit"),
frappe.provide("frappe.treeview_settings");
frappe.treeview_settings['Healthcare Service Unit'] = {
breadcrumbs: 'Healthcare Service Unit',
title: __('Service Unit Tree'),
get_tree_root: false,
filters: [{
fieldname: "company",
fieldtype: "Select",
options: erpnext.utils.get_tree_options("company"),
label: __("Company"),
default: erpnext.utils.get_tree_default("company")
}],
get_tree_nodes: 'erpnext.healthcare.utils.get_children',
ignore_fields:["parent_healthcare_service_unit"],
onrender: function(node) {
if (node.data.occupied_out_of_vacant!==undefined) {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupied_out_of_vacant
filters: [{
fieldname: 'company',
fieldtype: 'Select',
options: erpnext.utils.get_tree_options('company'),
label: __('Company'),
default: erpnext.utils.get_tree_default('company')
}],
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('New Service Unit Name'),
reqd: true
},
{
fieldtype: 'Check', fieldname: 'is_group', label: __('Is Group'),
description: __("Child nodes can be only created under 'Group' type nodes")
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '',
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
depends_on: 'eval:!doc.is_group'
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.value;
}
}
],
ignore_fields: ['parent_healthcare_service_unit'],
onrender: function (node) {
if (node.data.occupied_of_available !== undefined) {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupied_of_available
+ '</span>').insertBefore(node.$ul);
}
if (node.data && node.data.inpatient_occupancy!==undefined) {
if (node.data && node.data.inpatient_occupancy !== undefined) {
if (node.data.inpatient_occupancy == 1) {
if (node.data.occupancy_status == "Occupied") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Occupied') {
$("<span class='balance-area pull-right small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
if (node.data.occupancy_status == "Vacant") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Vacant') {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
}
}
},
post_render: function (treeview) {
frappe.treeview_settings['Healthcare Service Unit'].treeview = {};
$.extend(frappe.treeview_settings['Healthcare Service Unit'].treeview, treeview);
},
toolbar: [
{
label: __('Add Multiple'),
condition: function (node) {
return node.expandable;
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __('Add Multiple Service Units'),
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('Service Unit Name'),
reqd: true, description: __("Will be serially suffixed to maintain uniquness. Example: 'Ward' will be named as 'Ward-####'"),
},
{
fieldtype: 'Int', fieldname: 'count', label: __('Number of Service Units'),
reqd: true
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '', reqd: true,
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.get_value();
}
}
],
primary_action: () => {
dialog.hide();
let vals = dialog.get_values();
if (!vals) return;
return frappe.call({
method: 'erpnext.healthcare.doctype.healthcare_service_unit.healthcare_service_unit.add_multiple_service_units',
args: {
parent: node.data.value,
data: vals
},
callback: function (r) {
if (!r.exc && r.message) {
frappe.treeview_settings['Healthcare Service Unit'].treeview.tree.load_children(node, true);
frappe.show_alert({
message: __('{0} Service Units created', [vals.count - r.message.length]),
indicator: 'green'
});
} else {
frappe.msgprint(__('Could not create Service Units'));
}
},
freeze: true,
freeze_message: __('Creating {0} Service Units', [vals.count])
});
},
primary_action_label: __('Create')
});
dialog.show();
}
}
],
extend_toolbar: true
};

View File

@ -68,8 +68,8 @@ let change_item_code = function(frm, doc) {
if (values) {
frappe.call({
"method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code",
"args": {item: doc.item, item_code: values['item_code'], doc_name: doc.name},
callback: function () {
"args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name },
callback: function() {
frm.reload_doc();
}
});

View File

@ -29,6 +29,8 @@
{
"fieldname": "service_unit_type",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Service Unit Type",
"no_copy": 1,
@ -41,6 +43,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Appointments"
},
{
@ -49,6 +53,8 @@
"depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap"
},
{
@ -57,6 +63,8 @@
"depends_on": "eval:doc.allow_appointments != 1",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Inpatient Occupancy"
},
{
@ -65,17 +73,23 @@
"depends_on": "eval:doc.inpatient_occupancy == 1 && doc.allow_appointments != 1",
"fieldname": "is_billable",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Billable"
},
{
"depends_on": "is_billable",
"fieldname": "item_details",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Details"
},
{
"fieldname": "item",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item",
"no_copy": 1,
"options": "Item",
@ -84,6 +98,8 @@
{
"fieldname": "item_code",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Code",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"no_copy": 1
@ -91,6 +107,8 @@
{
"fieldname": "item_group",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Group",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "Item Group"
@ -98,6 +116,8 @@
{
"fieldname": "uom",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "UOM"
@ -105,28 +125,38 @@
{
"fieldname": "no_of_hours",
"fieldtype": "Int",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM Conversion in Hours",
"mandatory_depends_on": "eval: doc.is_billable == 1"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Rate / UOM"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Disabled",
"no_copy": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "Description"
},
{
@ -134,11 +164,13 @@
"fieldname": "change_in_item",
"fieldtype": "Check",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Change in Item"
}
],
"links": [],
"modified": "2020-05-20 15:31:09.627516",
"modified": "2021-08-19 17:52:30.266667",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit Type",

View File

@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None):
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.healthcare_service_unit_name = unit_name or "_Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None):
service_unit.is_group = 0
service_unit_parent_name = frappe.db.exists({
"doctype": "Healthcare Service Unit",
"healthcare_service_unit_name": "All Healthcare Service Units",
"healthcare_service_unit_name": "_Test All Healthcare Service Units",
"is_group": 1
})
if not service_unit_parent_name:
parent_service_unit = frappe.new_doc("Healthcare Service Unit")
parent_service_unit.healthcare_service_unit_name = "All Healthcare Service Units"
parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units"
parent_service_unit.is_group = 1
parent_service_unit.save(ignore_permissions = True)
service_unit.parent_healthcare_service_unit = parent_service_unit.name
@ -180,7 +180,7 @@ def get_service_unit_type():
if not service_unit_type:
service_unit_type = frappe.new_doc("Healthcare Service Unit Type")
service_unit_type.service_unit_type = "Test Service Unit Type Ip Occupancy"
service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy"
service_unit_type.inpatient_occupancy = 1
service_unit_type.save(ignore_permissions = True)
return service_unit_type.name

View File

@ -34,7 +34,7 @@ class LabTest(Document):
frappe.db.set_value('Lab Prescription', self.prescription, 'lab_test_created', 1)
if frappe.db.get_value('Lab Prescription', self.prescription, 'invoiced'):
self.invoiced = True
if not self.lab_test_name and self.template:
if self.template:
self.load_test_from_template()
self.reload()
@ -50,7 +50,7 @@ class LabTest(Document):
item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor)
except:
item.secondary_uom_result = ''
frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning'))
frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated').format(item.idx), title = _('Warning'))
def validate_result_values(self):
if self.normal_test_items:
@ -229,9 +229,9 @@ def create_sample_doc(template, patient, invoice, company = None):
sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0])
quantity = int(sample_collection.sample_qty) + int(template.sample_qty)
if template.sample_details:
sample_details = sample_collection.sample_details + '\n-\n' + _('Test: ')
sample_details = sample_collection.sample_details + '\n-\n' + _('Test :')
sample_details += (template.get('lab_test_name') or template.get('template')) + '\n'
sample_details += _('Collection Details: ') + '\n\t' + template.sample_details
sample_details += _('Collection Details:') + '\n\t' + template.sample_details
frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_details', sample_details)
frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_qty', quantity)

View File

@ -26,31 +26,39 @@ frappe.ui.form.on('Patient', {
}
if (frm.doc.patient_name && frappe.user.has_role('Physician')) {
frm.add_custom_button(__('Patient Progress'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient-progress');
}, __('View'));
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient_history');
},'View');
}, __('View'));
}
if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Vital Signs'), function () {
create_vital_signs(frm);
}, 'Create');
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.add_custom_button(__('Patient Encounter'), function () {
create_encounter(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'};
frm.toggle_display(['address_html', 'contact_html'], !frm.is_new());
if (!frm.is_new()) {
if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0);
}
frappe.contacts.render_address_and_contact(frm);
erpnext.utils.set_party_dashboard_indicators(frm);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
},
onload: function (frm) {
if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
if (frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
}
});
@ -59,16 +67,14 @@ frappe.ui.form.on('Patient', 'dob', function(frm) {
if (frm.doc.dob) {
let today = new Date();
let birthDate = new Date(frm.doc.dob);
if (today < birthDate){
if (today < birthDate) {
frappe.msgprint(__('Please select a valid Date'));
frappe.model.set_value(frm.doctype,frm.docname, 'dob', '');
}
else {
} else {
let age_str = get_age(frm.doc.dob);
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
});

View File

@ -1,6 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"allow_events_in_timeline": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@ -24,12 +24,19 @@
"image",
"column_break_14",
"status",
"uid",
"inpatient_record",
"inpatient_status",
"report_preference",
"mobile",
"email",
"phone",
"email",
"invite_user",
"user_id",
"address_contacts",
"address_html",
"column_break_22",
"contact_html",
"customer_details_section",
"customer",
"customer_group",
@ -74,6 +81,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Inpatient Status",
"no_copy": 1,
"options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled",
"read_only": 1
},
@ -81,6 +89,7 @@
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"no_copy": 1,
"options": "Inpatient Record",
"read_only": 1
},
@ -101,6 +110,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name",
"no_copy": 1,
"read_only": 1,
"search_index": 1
},
@ -118,6 +128,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Blood Group",
"no_copy": 1,
"options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative"
},
{
@ -125,7 +136,8 @@
"fieldname": "dob",
"fieldtype": "Date",
"in_preview": 1,
"label": "Date of birth"
"label": "Date of birth",
"no_copy": 1
},
{
"fieldname": "age_html",
@ -167,6 +179,7 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Customer",
"no_copy": 1,
"options": "Customer",
"set_only_once": 1
},
@ -183,6 +196,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Mobile",
"no_copy": 1,
"options": "Phone"
},
{
@ -192,6 +206,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"no_copy": 1,
"options": "Email"
},
{
@ -199,6 +214,7 @@
"fieldtype": "Data",
"in_filter": 1,
"label": "Phone",
"no_copy": 1,
"options": "Phone"
},
{
@ -230,7 +246,8 @@
"fieldname": "medication",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medication"
"label": "Medication",
"no_copy": 1
},
{
"fieldname": "column_break_20",
@ -240,13 +257,15 @@
"fieldname": "medical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medical History"
"label": "Medical History",
"no_copy": 1
},
{
"fieldname": "surgical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Surgical History"
"label": "Surgical History",
"no_copy": 1
},
{
"collapsible": 1,
@ -258,8 +277,8 @@
"fieldname": "occupation",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"in_standard_filter": 1,
"label": "Occupation"
"label": "Occupation",
"no_copy": 1
},
{
"fieldname": "column_break_25",
@ -269,6 +288,7 @@
"fieldname": "marital_status",
"fieldtype": "Select",
"label": "Marital Status",
"no_copy": 1,
"options": "\nSingle\nMarried\nDivorced\nWidow"
},
{
@ -281,25 +301,29 @@
"fieldname": "tobacco_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Past)"
"label": "Tobacco Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "tobacco_current_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Present)"
"label": "Tobacco Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "alcohol_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Alcohol Consumption (Past)"
"label": "Alcohol Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "alcohol_current_use",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Alcohol Consumption (Present)"
"label": "Alcohol Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "column_break_32",
@ -309,13 +333,15 @@
"fieldname": "surrounding_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Occupational Hazards and Environmental Factors"
"label": "Occupational Hazards and Environmental Factors",
"no_copy": 1
},
{
"fieldname": "other_risk_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Other Risk Factors"
"label": "Other Risk Factors",
"no_copy": 1
},
{
"collapsible": 1,
@ -331,7 +357,8 @@
"fieldname": "patient_details",
"fieldtype": "Text",
"ignore_xss_filter": 1,
"label": "Patient Details"
"label": "Patient Details",
"no_copy": 1
},
{
"fieldname": "default_currency",
@ -342,19 +369,22 @@
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name"
"label": "Last Name",
"no_copy": 1
},
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name",
"no_copy": 1,
"oldfieldtype": "Data",
"reqd": 1
},
{
"fieldname": "middle_name",
"fieldtype": "Data",
"label": "Middle Name (optional)"
"label": "Middle Name (optional)",
"no_copy": 1
},
{
"collapsible": 1,
@ -389,13 +419,63 @@
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "address_contacts",
"fieldtype": "Section Break",
"label": "Address and Contact",
"options": "fa fa-map-marker"
},
{
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML",
"no_copy": 1,
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"default": "1",
"fieldname": "invite_user",
"fieldtype": "Check",
"label": "Invite as User",
"no_copy": 1,
"read_only_depends_on": "eval: doc.user_id"
},
{
"fieldname": "user_id",
"fieldtype": "Read Only",
"label": "User ID",
"no_copy": 1,
"options": "User"
},
{
"allow_in_quick_entry": 1,
"bold": 1,
"fieldname": "uid",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Identification Number (UID)",
"unique": 1
}
],
"icon": "fa fa-user",
"image_field": "image",
"links": [],
"max_attachments": 50,
"modified": "2020-04-25 17:24:32.146415",
"modified": "2021-03-14 13:21:09.759906",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient",
@ -453,7 +533,7 @@
],
"quick_entry": 1,
"restrict_to_domain": "Healthcare",
"search_fields": "patient_name,mobile,email,phone",
"search_fields": "patient_name,mobile,email,phone,uid",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@ -8,24 +8,27 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, getdate
import dateutil
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.naming import set_name_by_naming_series
from frappe.utils.nestedset import get_root_of
from erpnext import get_default_currency
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms
from erpnext.accounts.party import get_dashboard_info
class Patient(Document):
def onload(self):
'''Load address and contacts in `__onload`'''
load_address_and_contact(self)
self.load_dashboard_info()
def validate(self):
self.set_full_name()
self.add_as_website_user()
def before_insert(self):
self.set_missing_customer_details()
def after_insert(self):
self.add_as_website_user()
self.reload()
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient') and not self.customer:
create_customer(self)
if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'):
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
else:
@ -49,6 +52,16 @@ class Patient(Document):
else:
create_customer(self)
self.set_contact() # add or update contact
if not self.user_id and self.email and self.invite_user:
self.create_website_user()
def load_dashboard_info(self):
if self.customer:
info = get_dashboard_info('Customer', self.customer, None)
self.set_onload('dashboard_info', info)
def set_full_name(self):
if self.last_name:
self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name]))
@ -71,18 +84,24 @@ class Patient(Document):
if not self.language:
self.language = frappe.db.get_single_value('System Settings', 'language')
def add_as_website_user(self):
if self.email:
if not frappe.db.exists ('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User'
})
user.flags.ignore_permissions = True
user.add_roles('Patient')
def create_website_user(self):
if self.email and not frappe.db.exists('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User',
'gender': self.sex,
'phone': self.phone,
'mobile_no': self.mobile,
'birth_date': self.dob
})
user.flags.ignore_permissions = True
user.enabled = True
user.send_welcome_email = True
user.add_roles('Patient')
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name)
def autoname(self):
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
@ -114,7 +133,7 @@ class Patient(Document):
age = self.age
if not age:
return
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age_str = f'{str(age.years)} {_("Years(s)")} {str(age.months)} {_("Month(s)")} {str(age.days)} {_("Day(s)")}'
return age_str
@frappe.whitelist()
@ -131,6 +150,58 @@ class Patient(Document):
return {'invoice': sales_invoice.name}
def set_contact(self):
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}):
old_doc = self.get_doc_before_save()
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
self.update_contact()
else:
self.reload()
if self.email or self.mobile or self.phone:
contact = frappe.get_doc({
'doctype': 'Contact',
'first_name': self.first_name,
'middle_name': self.middle_name,
'last_name': self.last_name,
'gender': self.sex,
'is_primary_contact': 1
})
contact.append('links', dict(link_doctype='Patient', link_name=self.name))
if self.customer:
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
contact.insert(ignore_permissions=True)
self.update_contact(contact) # update email, mobile and phone
def update_contact(self, contact=None):
if not contact:
contact_name = get_default_contact(self.doctype, self.name)
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
if contact:
if self.email and self.email != contact.email_id:
for email in contact.email_ids:
email.is_primary = True if email.email_id == self.email else False
contact.add_email(self.email, is_primary=True)
contact.set_primary_email()
if self.mobile and self.mobile != contact.mobile_no:
for mobile in contact.phone_nos:
mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False
contact.add_phone(self.mobile, is_primary_mobile_no=True)
contact.set_primary('mobile_no')
if self.phone and self.phone != contact.phone:
for phone in contact.phone_nos:
phone.is_primary_phone = True if phone.phone == self.phone else False
contact.add_phone(self.phone, is_primary_phone=True)
contact.set_primary('phone')
contact.flags.ignore_validate = True # disable hook TODO: safe?
contact.save(ignore_permissions=True)
def create_customer(doc):
customer = frappe.get_doc({
'doctype': 'Customer',
@ -156,8 +227,8 @@ def make_invoice(patient, company):
sales_invoice.debit_to = get_receivable_account(company)
item_line = sales_invoice.append('items')
item_line.item_name = 'Registeration Fee'
item_line.description = 'Registeration Fee'
item_line.item_name = 'Registration Fee'
item_line.description = 'Registration Fee'
item_line.qty = 1
item_line.uom = uom
item_line.conversion_factor = 1
@ -181,8 +252,11 @@ def get_patient_detail(patient):
return details
def get_timeline_data(doctype, name):
"""Return timeline data from medical records"""
return dict(frappe.db.sql('''
'''
Return Patient's timeline data from medical records
Also include the associated Customer timeline data
'''
patient_timeline_data = dict(frappe.db.sql('''
SELECT
unix_timestamp(communication_date), count(*)
FROM
@ -191,3 +265,11 @@ def get_timeline_data(doctype, name):
patient=%s
and `communication_date` > date_sub(curdate(), interval 1 year)
GROUP BY communication_date''', name))
customer = frappe.db.get_value(doctype, name, 'customer')
if customer:
from erpnext.accounts.party import get_timeline_data
customer_timeline_data = get_timeline_data('Customer', customer)
patient_timeline_data.update(customer_timeline_data)
return patient_timeline_data

View File

@ -6,22 +6,33 @@ def get_data():
'heatmap': True,
'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'),
'fieldname': 'patient',
'non_standard_fieldnames': {
'Payment Entry': 'party'
},
'transactions': [
{
'label': _('Appointments and Patient Encounters'),
'items': ['Patient Appointment', 'Patient Encounter']
'label': _('Appointments and Encounters'),
'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter']
},
{
'label': _('Lab Tests and Vital Signs'),
'items': ['Lab Test', 'Sample Collection', 'Vital Signs']
'items': ['Lab Test', 'Sample Collection']
},
{
'label': _('Billing'),
'items': ['Sales Invoice']
'label': _('Rehab and Physiotherapy'),
'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan']
},
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
'label': _('Surgery'),
'items': ['Clinical Procedure']
},
{
'label': _('Admissions'),
'items': ['Inpatient Record', 'Inpatient Medication Order']
},
{
'label': _('Billing and Payments'),
'items': ['Sales Invoice', 'Payment Entry']
}
]
}

View File

@ -17,9 +17,9 @@ frappe.ui.form.on('Patient Appointment', {
},
refresh: function(frm) {
frm.set_query('patient', function () {
frm.set_query('patient', function() {
return {
filters: {'status': 'Active'}
filters: { 'status': 'Active' }
};
});
@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', {
} else {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message == true) {
if (frm.doc.mode_of_payment && frm.doc.paid_amount) {
@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.patient};
frappe.route_options = { 'patient': frm.doc.patient };
frappe.set_route('patient_history');
}, __('View'));
}
@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', {
});
if (frm.doc.procedure_template) {
frm.add_custom_button(__('Clinical Procedure'), function(){
frm.add_custom_button(__('Clinical Procedure'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure',
frm: frm,
});
}, __('Create'));
} else if (frm.doc.therapy_type) {
frm.add_custom_button(__('Therapy Session'),function(){
frm.add_custom_button(__('Therapy Session'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session',
frm: frm,
@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', {
doctype: 'Patient',
name: frm.doc.patient
},
callback: function (data) {
callback: function(data) {
let age = null;
if (data.message.dob) {
age = calculate_age(data.message.dob);
@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', {
},
practitioner: function(frm) {
if (frm.doc.practitioner ) {
if (frm.doc.practitioner) {
frm.events.set_payment_details(frm);
}
},
@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', {
toggle_payment_fields: function(frm) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message.fee_validity) {
// if fee validity exists and automated appointment invoicing is enabled,
@ -254,7 +254,7 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_display('paid_amount', data.message ? 1 : 0);
frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
frm.toggle_reqd('paid_amount', data.message ? 1 : 0);
frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
@ -265,7 +265,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frappe.call({
method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies",
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message) {
show_therapy_types(frm, r.message);
@ -302,13 +302,13 @@ let check_and_set_availability = function(frm) {
let d = new frappe.ui.Dialog({
title: __('Available slots'),
fields: [
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'},
{ fieldtype: 'Section Break'},
{ fieldtype: 'HTML', fieldname: 'available_slots'}
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' },
{ fieldtype: 'Section Break' },
{ fieldtype: 'HTML', fieldname: 'available_slots' }
],
primary_action_label: __('Book'),
@ -386,59 +386,22 @@ let check_and_set_availability = function(frm) {
let $wrapper = d.fields_dict.available_slots.$wrapper;
// make buttons for each slot
let slot_details = data.slot_details;
let slot_html = '';
for (let i = 0; i < slot_details.length; i++) {
slot_html = slot_html + `<label>${slot_details[i].slot_name}</label>`;
slot_html = slot_html + `<br/>` + slot_details[i].avail_slot.map(slot => {
let disabled = '';
let start_str = slot.from_time;
let slot_start_time = moment(slot.from_time, 'HH:mm:ss');
let slot_to_time = moment(slot.to_time, 'HH:mm:ss');
let interval = (slot_to_time - slot_start_time)/60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_details[i].appointments.forEach(function(booked) {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_to_time)) {
if(booked.duration == 0){
disabled = 'disabled="disabled"';
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_start_time.isBefore(end_time) && slot_to_time.isAfter(booked_moment)) {
// There is an overlap
disabled = 'disabled="disabled"';
return false;
}
});
return `<button class="btn btn-default"
data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_details[i].service_unit || ''}"
style="margin: 0 10px 10px 0; width: 72px;" ${disabled}>
${start_str.substring(0, start_str.length - 3)}
</button>`;
}).join("");
slot_html = slot_html + `<br/>`;
}
let slot_html = get_slots(data.slot_details);
$wrapper
.css('margin-bottom', 0)
.addClass('text-center')
.html(slot_html);
// blue button when clicked
// highlight button when clicked
$wrapper.on('click', 'button', function() {
let $btn = $(this);
$wrapper.find('button').removeClass('btn-primary');
$btn.addClass('btn-primary');
$wrapper.find('button').removeClass('btn-outline-primary');
$btn.addClass('btn-outline-primary');
selected_slot = $btn.attr('data-name');
service_unit = $btn.attr('data-service-unit');
duration = $btn.attr('data-duration');
// enable dialog action
// enable primary action 'Book'
d.get_primary_btn().attr('disabled', null);
});
@ -448,19 +411,102 @@ let check_and_set_availability = function(frm) {
}
},
freeze: true,
freeze_message: __('Fetching records......')
freeze_message: __('Fetching Schedule...')
});
} else {
fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold());
}
}
function get_slots(slot_details) {
let slot_html = '';
let appointment_count = 0;
let disabled = false;
let start_str, slot_start_time, slot_end_time, interval, count, count_class, tool_tip, available_slots;
slot_details.forEach((slot_info) => {
slot_html += `<div class="slot-info">
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
if (slot_info.service_unit_capacity) {
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
}
slot_html += '</div><br><br>';
slot_html += slot_info.avail_slot.map(slot => {
appointment_count = 0;
disabled = false;
start_str = slot.from_time;
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
interval = (slot_end_time - slot_start_time) / 60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_info.appointments.forEach((booked) => {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_end_time)) {
if (booked.duration == 0) {
disabled = true;
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_info.allow_overlap != 1) {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
// There is an overlap
disabled = true;
return false;
}
} else {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
appointment_count++;
}
if (appointment_count >= slot_info.service_unit_capacity) {
// There is an overlap
disabled = true;
return false;
}
}
});
if (slot_info.allow_overlap == 1 && slot_info.service_unit_capacity > 1) {
available_slots = slot_info.service_unit_capacity - appointment_count;
count = `${(available_slots > 0 ? available_slots : __('Full'))}`;
count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`;
tool_tip =`${available_slots} ${__('slots available for booking')}`;
}
return `
<button class="btn btn-secondary" data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_info.service_unit || ''}"
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
data-toggle="tooltip" title="${tool_tip}">
${start_str.substring(0, start_str.length - 3)}<br>
<span class='badge ${count_class}'> ${count} </span>
</button>`;
}).join("");
if (slot_info.service_unit_capacity) {
slot_html += `<br/><small>${__('Each slot indicates the capacity currently available for booking')}</small>`;
}
slot_html += `<br/><br/>`;
});
return slot_html;
}
};
let get_prescribed_procedure = function(frm) {
if (frm.doc.patient) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed',
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message && r.message.length) {
show_procedure_templates(frm, r.message);
@ -480,7 +526,7 @@ let get_prescribed_procedure = function(frm) {
}
};
let show_procedure_templates = function(frm, result){
let show_procedure_templates = function(frm, result) {
let d = new frappe.ui.Dialog({
title: __('Prescribed Procedures'),
fields: [
@ -500,9 +546,11 @@ let show_procedure_templates = function(frm, result){
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {name:y[0], procedure_template: y[1],
encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4],
practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
name: y[0], procedure_template: y[1],
encounter: y[2], consulting_practitioner: y[3], encounter_date: y[4],
practitioner: y[5] ? y[5] : '', date: y[6] ? y[6] : '', department: y[7] ? y[7] : ''
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.procedure_template = $(this).attr('data-procedure-template');
frm.doc.procedure_prescription = $(this).attr('data-name');
@ -520,7 +568,7 @@ let show_procedure_templates = function(frm, result){
});
if (!result) {
let msg = __('There are no procedure prescribed for ') + frm.doc.patient;
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', {msg: msg})).appendTo(html_field);
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', { msg: msg })).appendTo(html_field);
}
d.show();
};
@ -535,7 +583,7 @@ let show_therapy_types = function(frm, result) {
]
});
var html_field = d.fields_dict.therapy_type.$wrapper;
$.each(result, function(x, y){
$.each(result, function(x, y) {
var row = $(repl('<div class="col-xs-12" style="padding-top:12px; text-align:center;" >\
<div class="col-xs-5"> %(encounter)s <br> %(practitioner)s <br> %(date)s </div>\
<div class="col-xs-5"> %(therapy)s </div>\
@ -544,9 +592,11 @@ let show_therapy_types = function(frm, result) {
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {therapy:y[0],
name: y[1], encounter:y[2], practitioner:y[3], date:y[4],
department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
therapy: y[0],
name: y[1], encounter: y[2], practitioner: y[3], date: y[4],
department: y[6] ? y[6] : '', therapy_plan: y[5]
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.therapy_type = $(this).attr("data-therapy");
@ -581,13 +631,13 @@ let create_vital_signs = function(frm) {
frappe.new_doc('Vital Signs');
};
let update_status = function(frm, status){
let update_status = function(frm, status) {
let doc = frm.doc;
frappe.confirm(__('Are you sure you want to cancel this appointment?'),
function() {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status',
args: {appointment_id: doc.name, status:status},
args: { appointment_id: doc.name, status: status },
callback: function(data) {
if (!data.exc) {
frm.reload_doc();

View File

@ -131,7 +131,7 @@
"fieldtype": "Link",
"label": "Service Unit",
"options": "Healthcare Service Unit",
"set_only_once": 1
"read_only": 1
},
{
"depends_on": "eval:doc.practitioner;",
@ -349,7 +349,7 @@
}
],
"links": [],
"modified": "2021-06-16 00:40:26.841794",
"modified": "2021-08-30 09:00:41.329387",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from frappe.utils import getdate, get_time, flt
from frappe.utils import getdate, get_time, flt, get_link_to_form
from frappe.model.mapper import get_mapped_doc
from frappe import _
import datetime
@ -15,6 +15,11 @@ from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity
class MaximumCapacityError(frappe.ValidationError):
pass
class OverlapError(frappe.ValidationError):
pass
class PatientAppointment(Document):
def validate(self):
self.validate_overlaps()
@ -49,26 +54,49 @@ class PatientAppointment(Document):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select
name, practitioner, patient, appointment_time, duration
from
`tabPatient Appointment`
where
appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled")
and (practitioner=%s or patient=%s) and
((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or
(appointment_time>%s and appointment_time<%s) or
(appointment_time=%s))
""", (self.appointment_date, self.name, self.practitioner, self.patient,
self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time))
# all appointments for both patient and practitioner overlapping the duration of this appointment
overlapping_appointments = frappe.db.sql("""
SELECT
name, practitioner, patient, appointment_time, duration, service_unit
FROM
`tabPatient Appointment`
WHERE
appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND
(practitioner=%(practitioner)s OR patient=%(patient)s) AND
((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR
(appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR
(appointment_time=%(appointment_time)s))
""",
{
'appointment_date': self.appointment_date,
'name': self.name,
'practitioner': self.practitioner,
'patient': self.patient,
'appointment_time': self.appointment_time,
'end_time':end_time.time()
},
as_dict = True
)
if not overlapping_appointments:
return # No overlaps, nothing to validate!
if self.service_unit: # validate service unit capacity if overlap enabled
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit,
['overlap_appointments', 'service_unit_capacity'])
if allow_overlap:
service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and
appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap
if len(service_unit_appointments) >= (service_unit_capacity or 1):
frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}")
.format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError)
else: # service_unit_appointments within capacity, remove from overlapping_appointments
overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments]
if overlapping_appointments:
frappe.throw(_("Not allowed, cannot overlap appointment {}")
.format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError)
if overlaps:
overlapping_details = _('Appointment overlaps with ')
overlapping_details += "<b><a href='/app/Form/Patient Appointment/{0}'>{0}</a></b><br>".format(overlaps[0][0])
overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format(
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
@ -305,17 +333,13 @@ def check_employee_wise_availability(date, practitioner_doc):
def get_available_slots(practitioner_doc, date):
available_slots = []
slot_details = []
available_slots = slot_details = []
weekday = date.strftime('%A')
practitioner = practitioner_doc.name
for schedule_entry in practitioner_doc.practitioner_schedules:
if schedule_entry.schedule:
practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule)
else:
frappe.throw(_('{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner').format(
frappe.bold(practitioner)), title=_('Practitioner Schedule Not Found'))
validate_practitioner_schedules(schedule_entry, practitioner)
practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule)
if practitioner_schedule:
available_slots = []
@ -325,6 +349,8 @@ def get_available_slots(practitioner_doc, date):
if available_slots:
appointments = []
allow_overlap = 0
service_unit_capacity = 0
# fetch all appointments to practitioner by service unit
filters = {
'practitioner': practitioner,
@ -334,8 +360,8 @@ def get_available_slots(practitioner_doc, date):
}
if schedule_entry.service_unit:
slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit
allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments')
slot_name = f'{schedule_entry.schedule}'
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity'])
if not allow_overlap:
# fetch all appointments to service unit
filters.pop('practitioner')
@ -350,12 +376,25 @@ def get_available_slots(practitioner_doc, date):
filters=filters,
fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit,
'avail_slot':available_slots, 'appointments': appointments})
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
return slot_details
def validate_practitioner_schedules(schedule_entry, practitioner):
if schedule_entry.schedule:
if not schedule_entry.service_unit:
frappe.throw(_('Practitioner {0} does not have a Service Unit set against the Practitioner Schedule {1}.').format(
get_link_to_form('Healthcare Practitioner', practitioner), frappe.bold(schedule_entry.schedule)),
title=_('Service Unit Not Found'))
else:
frappe.throw(_('Practitioner {0} does not have a Practitioner Schedule assigned.').format(
get_link_to_form('Healthcare Practitioner', practitioner)),
title=_('Practitioner Schedule Not Found'))
@frappe.whitelist()
def update_status(appointment_id, status):
frappe.db.set_value('Patient Appointment', appointment_id, 'status', status)

View File

@ -16,9 +16,11 @@ class TestPatientAppointment(unittest.TestCase):
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""")
make_pos_profile()
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""")
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""")
def test_status(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
self.assertEqual(appointment.status, 'Open')
@ -30,7 +32,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
appointment.reload()
@ -44,7 +46,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
@ -60,13 +62,14 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_based_on_department(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment_type = create_appointment_type()
appointment_type = create_appointment_type({'medical_department': medical_department})
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
invoice=1, appointment_type=appointment_type.name, department=medical_department)
appointment.reload()
self.assertEqual(appointment.invoiced, 1)
@ -78,7 +81,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_according_to_appointment_type_charge(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
@ -88,9 +91,9 @@ class TestPatientAppointment(unittest.TestCase):
'op_consulting_charge': 300
}]
appointment_type = create_appointment_type(args={
'name': 'Generic Appointment Type charge',
'items': items
})
'name': 'Generic Appointment Type charge',
'items': items
})
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
invoice=1, appointment_type=appointment_type.name)
@ -104,7 +107,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(sales_invoice_name)
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
appointment = create_appointment(patient, practitioner, nowdate())
fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner})
@ -112,7 +115,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(fee_validity)
# first follow up appointment
appointment = create_appointment(patient, practitioner, nowdate())
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1))
self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1)
update_status(appointment.name, 'Cancelled')
@ -121,7 +124,7 @@ class TestPatientAppointment(unittest.TestCase):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1)
update_status(appointment.name, 'Cancelled')
# check invoice cancelled
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
@ -133,7 +136,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@ -141,7 +144,7 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
@ -159,7 +162,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@ -167,10 +170,10 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
@ -192,7 +195,7 @@ class TestPatientAppointment(unittest.TestCase):
assert payment_required is True
def test_sales_invoice_should_be_generated_for_new_patient_appointment(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
invoice_count = frappe.db.count('Sales Invoice')
@ -203,10 +206,10 @@ class TestPatientAppointment(unittest.TestCase):
assert new_invoice_count == invoice_count + 1
def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
create_appointment(patient, practitioner, nowdate())
patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John')
patient, new_practitioner = create_healthcare_docs(id=5)
create_appointment(patient, new_practitioner, nowdate())
roles = [{"doctype": "Has Role", "role": "Physician"}]
@ -223,41 +226,102 @@ class TestPatientAppointment(unittest.TestCase):
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 2
def create_healthcare_docs(practitioner_name=None):
if not practitioner_name:
practitioner_name = '_Test Healthcare Practitioner'
def test_overlap_appointment(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError
patient, practitioner = create_healthcare_docs(id=1)
patient_1, practitioner_1 = create_healthcare_docs(id=2)
service_unit = create_service_unit(id=0)
service_unit_1 = create_service_unit(id=1)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid
patient = create_patient()
practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name)
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department')
# patient and practitioner cannot have overlapping appointments
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link
self.assertRaises(OverlapError, appointment.save)
if not medical_department:
medical_department = frappe.new_doc('Medical Department')
medical_department.department = '_Test Medical Department'
medical_department.save(ignore_permissions=True)
medical_department = medical_department.name
# patient cannot have overlapping appointments with other practitioners
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
if not practitioner:
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = practitioner_name
practitioner.gender = 'Female'
practitioner.department = medical_department
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
practitioner = practitioner.name
# practitioner cannot have overlapping appointments with other patients
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
return patient, medical_department, practitioner
def test_service_unit_capacity(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import MaximumCapacityError, OverlapError
practitioner = create_practitioner()
capacity = 3
overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1)
overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity)
for i in range(0, capacity):
patient = create_patient(id=i)
create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap
self.assertRaises(OverlapError, appointment.save)
patient = create_patient(id=capacity)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0)
self.assertRaises(MaximumCapacityError, appointment.save)
def create_healthcare_docs(id=0):
patient = create_patient(id)
practitioner = create_practitioner(id)
return patient, practitioner
def create_patient(id=0):
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
return patient
patient = frappe.new_doc('Patient')
patient.first_name = f'_Test Patient {str(id)}'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
return patient.name
def create_medical_department(id=0):
if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'):
return f'_Test Medical Department {str(id)}'
medical_department = frappe.new_doc('Medical Department')
medical_department.department = f'_Test Medical Department {str(id)}'
medical_department.save(ignore_permissions=True)
return medical_department.name
def create_practitioner(id=0, medical_department=None):
if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}):
practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name'])
return practitioner
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}'
practitioner.gender = 'Female'
practitioner.department = medical_department or create_medical_department(id)
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
return practitioner.name
def create_patient():
patient = frappe.db.exists('Patient', '_Test Patient')
if not patient:
patient = frappe.new_doc('Patient')
patient.first_name = '_Test Patient'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
patient = patient.name
return patient
def create_encounter(appointment):
if appointment:
@ -270,8 +334,10 @@ def create_encounter(appointment):
encounter.company = appointment.company
encounter.save()
encounter.submit()
return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
@ -284,6 +350,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice:
@ -294,11 +361,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
appointment.save(ignore_permissions=True)
return appointment
def create_healthcare_service_items():
if frappe.db.exists('Item', 'HLC-SI-001'):
return 'HLC-SI-001'
item = frappe.new_doc('Item')
item.item_code = 'HLC-SI-001'
item.item_name = 'Consulting Charges'
@ -306,11 +376,14 @@ def create_healthcare_service_items():
item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save()
return item.name
def create_clinical_procedure_template():
if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'):
return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab')
template = frappe.new_doc('Clinical Procedure Template')
template.template = 'Knee Surgery and Rehab'
template.item_code = 'Knee Surgery and Rehab'
@ -319,8 +392,10 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
return template
def create_appointment_type(args=None):
if not args:
args = frappe.local.form_dict
@ -333,9 +408,9 @@ def create_appointment_type(args=None):
else:
item = create_healthcare_service_items()
items = [{
'medical_department': '_Test Medical Department',
'op_consulting_charge_item': item,
'op_consulting_charge': 200
'medical_department': args.get('medical_department') or '_Test Medical Department',
'op_consulting_charge_item': item,
'op_consulting_charge': 200
}]
return frappe.get_doc({
'doctype': 'Appointment Type',
@ -359,3 +434,30 @@ def create_user(email=None, roles=None):
"roles": roles,
}).insert()
return user
def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0):
if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'):
return f'_Test Service Unit Type {str(id)}'
service_unit_type = frappe.new_doc('Healthcare Service Unit Type')
service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}'
service_unit_type.allow_appointments = allow_appointments
service_unit_type.overlap_appointments = overlap_appointments
service_unit_type.save(ignore_permissions=True)
return service_unit_type.name
def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0):
if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'):
return f'_Test service_unit {str(id)}'
service_unit = frappe.new_doc('Healthcare Service Unit')
service_unit.is_group = 0
service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}'
service_unit.service_unit_type = service_unit_type or create_service_unit_type(id)
service_unit.service_unit_capacity = service_unit_capacity
service_unit.save(ignore_permissions=True)
return service_unit.name

View File

@ -18,7 +18,7 @@ class PatientHistorySettings(Document):
def validate_submittable_doctypes(self):
for entry in self.custom_doctypes:
if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')):
msg = _('Row #{0}: Document Type {1} is not submittable. ').format(
msg = _('Row #{0}: Document Type {1} is not submittable.').format(
entry.idx, frappe.bold(entry.document_type))
msg += _('Patient Medical Record can only be created for submittable document types.')
frappe.throw(msg)
@ -116,12 +116,12 @@ def set_subject_field(doc):
fieldname = entry.get('fieldname')
if entry.get('fieldtype') == 'Table' and doc.get(fieldname):
formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname))
subject += frappe.bold(_(entry.get('label')) + ': ') + '<br>' + cstr(formatted_value) + '<br>'
subject += frappe.bold(_(entry.get('label')) + ':') + '<br>' + cstr(formatted_value) + '<br>'
else:
if doc.get(fieldname):
formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc)
subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '<br>'
subject += frappe.bold(_(entry.get('label')) + ':') + cstr(formatted_value) + '<br>'
return subject

View File

@ -38,13 +38,12 @@ class TestPatientHistorySettings(unittest.TestCase):
# tests for medical record creation of standard doctypes in test_patient_medical_record.py
patient = create_patient()
doc = create_doc(patient)
# check for medical record
medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name})
self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format(
frappe.utils.format_date(getdate()))
self.assertEqual(strip_html(medical_rec.subject), expected_subject)
self.assertEqual(medical_rec.patient, patient)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment, create_medical_department
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase):
@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase):
make_pos_profile()
def test_medical_record(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment)

View File

@ -8,11 +8,13 @@ import unittest
from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import \
create_healthcare_docs, create_patient, create_appointment, create_medical_department
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
encounter = create_encounter(patient, medical_department, practitioner)
self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan))
@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
patient, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session)
session.submit()

View File

@ -34,7 +34,8 @@ def create_therapy_type():
})
therapy_type.save()
else:
therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab')
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
return therapy_type
def create_exercise_type():
@ -47,4 +48,7 @@ def create_exercise_type():
'description': 'Squat and Rise'
})
exercise_type.save()
else:
exercise_type = frappe.get_doc('Exercise Type', exercise_type)
return exercise_type

View File

@ -10,7 +10,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/healthcare",
"idx": 0,
"is_complete": 0,
"modified": "2020-07-08 14:06:19.512946",
"modified": "2021-01-30 19:22:20.273766",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",

View File

@ -5,14 +5,14 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-26 23:16:31.965521",
"modified": "2021-01-30 12:02:22.849260",
"modified_by": "Administrator",
"name": "Create Healthcare Practitioner",
"owner": "Administrator",
"reference_document": "Healthcare Practitioner",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Healthcare Practitioner",
"validate_action": 1

View File

@ -5,14 +5,14 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-19 12:26:24.023418",
"modified_by": "Administrator",
"modified": "2021-01-30 00:09:28.786428",
"modified_by": "ruchamahabal2@gmail.com",
"name": "Create Patient",
"owner": "Administrator",
"reference_document": "Patient",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Patient",
"validate_action": 1

View File

@ -5,14 +5,14 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-19 12:27:09.437825",
"modified_by": "Administrator",
"modified": "2021-01-30 00:09:28.794602",
"modified_by": "ruchamahabal2@gmail.com",
"name": "Create Practitioner Schedule",
"owner": "Administrator",
"reference_document": "Practitioner Schedule",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Practitioner Schedule",
"validate_action": 1

View File

@ -5,14 +5,14 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-26 23:10:24.504030",
"modified": "2021-01-30 19:22:08.257160",
"modified_by": "Administrator",
"name": "Explore Clinical Procedure Templates",
"owner": "Administrator",
"reference_document": "Clinical Procedure Template",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Explore Clinical Procedure Templates",
"validate_action": 1

View File

@ -5,14 +5,14 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 1,
"is_skipped": 0,
"modified": "2020-05-26 23:10:24.507648",
"modified": "2021-01-30 19:22:07.275735",
"modified_by": "Administrator",
"name": "Explore Healthcare Settings",
"owner": "Administrator",
"reference_document": "Healthcare Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Explore Healthcare Settings",
"validate_action": 1

View File

@ -6,14 +6,14 @@
"field": "schedule",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-26 22:07:07.482530",
"modified_by": "Administrator",
"modified": "2021-01-30 00:09:28.807129",
"modified_by": "ruchamahabal2@gmail.com",
"name": "Introduction to Healthcare Practitioner",
"owner": "Administrator",
"reference_document": "Healthcare Practitioner",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Introduction to Healthcare Practitioner",
"validate_action": 0

View File

@ -9,6 +9,26 @@
cursor: pointer;
}
.patient-image-container {
margin-top: 17px;
}
.patient-image {
display: inline-block;
width: 100%;
height: 0;
padding: 50% 0px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
border-radius: 4px;
}
.patient-name {
font-size: 20px;
margin-top: 25px;
}
.medical_record-label {
max-width: 100px;
margin-bottom: -4px;
@ -19,19 +39,19 @@
}
.date-indicator {
background:none;
font-size:12px;
vertical-align:middle;
font-weight:bold;
color:#6c7680;
background:none;
font-size:12px;
vertical-align:middle;
font-weight:bold;
color:#6c7680;
}
.date-indicator::after {
margin:0 -4px 0 12px;
content:'';
display:inline-block;
height:8px;
width:8px;
border-radius:8px;
margin:0 -4px 0 12px;
content:'';
display:inline-block;
height:8px;
width:8px;
border-radius:8px;
background: #d1d8dd;
}

Some files were not shown because too many files have changed in this diff Show More