Merge branch 'develop' into gross-profit-product-bundle
This commit is contained in:
commit
82118975e2
7
.github/helper/install.sh
vendored
7
.github/helper/install.sh
vendored
@ -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
|
||||
|
||||
|
13
.github/workflows/patch.yml
vendored
13
.github/workflows/patch.yml
vendored
@ -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
|
||||
|
14
.github/workflows/server-tests.yml
vendored
14
.github/workflows/server-tests.yml
vendored
@ -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
|
||||
|
22
.github/workflows/translation_linter.yml
vendored
22
.github/workflows/translation_linter.yml
vendored
@ -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
|
6
.github/workflows/ui-tests.yml
vendored
6
.github/workflows/ui-tests.yml
vendored
@ -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:
|
||||
|
@ -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 => {
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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.")
|
||||
}
|
||||
];
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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)) {
|
||||
|
@ -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:
|
||||
|
@ -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}));
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
]
|
||||
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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."),
|
||||
}
|
||||
];
|
||||
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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.",
|
||||
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
22
erpnext/accounts/onboarding_step/company/company.json
Normal file
22
erpnext/accounts/onboarding_step/company/company.json
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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\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",
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
125
erpnext/assets/form_tour/asset/asset.json
Normal file
125
erpnext/assets/form_tour/asset/asset.json
Normal 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"
|
||||
}
|
65
erpnext/assets/form_tour/asset_category/asset_category.json
Normal file
65
erpnext/assets/form_tour/asset_category/asset_category.json
Normal 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"
|
||||
}
|
@ -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.",
|
||||
|
@ -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
|
||||
}
|
21
erpnext/assets/onboarding_step/asset_item/asset_item.json
Normal file
21
erpnext/assets/onboarding_step/asset_item/asset_item.json
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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')
|
||||
|
@ -13,7 +13,7 @@ def get_data():
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Opportunity', 'Quotation']
|
||||
'items': ['Opportunity', 'Quotation', 'Prospect']
|
||||
},
|
||||
]
|
||||
}
|
||||
|
28
erpnext/crm/doctype/lead/lead_list.js
Normal file
28
erpnext/crm/doctype/lead/lead_list.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -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"]],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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",
|
||||
|
0
erpnext/crm/doctype/prospect/__init__.py
Normal file
0
erpnext/crm/doctype/prospect/__init__.py
Normal file
29
erpnext/crm/doctype/prospect/prospect.js
Normal file
29
erpnext/crm/doctype/prospect/prospect.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
203
erpnext/crm/doctype/prospect/prospect.json
Normal file
203
erpnext/crm/doctype/prospect/prospect.json
Normal 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
|
||||
}
|
99
erpnext/crm/doctype/prospect/prospect.py
Normal file
99
erpnext/crm/doctype/prospect/prospect.py
Normal 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
|
54
erpnext/crm/doctype/prospect/test_prospect.py
Normal file
54
erpnext/crm/doctype/prospect/test_prospect.py
Normal 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
|
0
erpnext/crm/doctype/prospect_lead/__init__.py
Normal file
0
erpnext/crm/doctype/prospect_lead/__init__.py
Normal file
67
erpnext/crm/doctype/prospect_lead/prospect_lead.json
Normal file
67
erpnext/crm/doctype/prospect_lead/prospect_lead.json
Normal 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
|
||||
}
|
8
erpnext/crm/doctype/prospect_lead/prospect_lead.py
Normal file
8
erpnext/crm/doctype/prospect_lead/prospect_lead.py
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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": [
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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('');
|
||||
}
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user