Merge branch 'develop' into account-add-rename

This commit is contained in:
Marica 2020-09-29 13:18:54 +05:30 committed by GitHub
commit 25f4c1636e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
274 changed files with 5593 additions and 4492 deletions

View File

@ -16,7 +16,7 @@
ERPNext as a monolith includes the following areas for managing businesses: ERPNext as a monolith includes the following areas for managing businesses:
1. [Accounting](https://erpnext.com/open-source-accounting) 1. [Accounting](https://erpnext.com/open-source-accounting)
1. [Inventory](https://erpnext.com/distribution/inventory-management-system) 1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system)
1. [CRM](https://erpnext.com/open-source-crm) 1. [CRM](https://erpnext.com/open-source-crm)
1. [Sales](https://erpnext.com/open-source-sales-purchase) 1. [Sales](https://erpnext.com/open-source-sales-purchase)
1. [Purchase](https://erpnext.com/open-source-sales-purchase) 1. [Purchase](https://erpnext.com/open-source-sales-purchase)

View File

@ -98,7 +98,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"modified": "2020-06-19 12:42:44.054598", "modified": "2020-09-09 11:45:33.766400",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
@ -147,11 +147,6 @@
"link_to": "Trial Balance", "link_to": "Trial Balance",
"type": "Report" "type": "Report"
}, },
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{ {
"label": "Dashboard", "label": "Dashboard",
"link_to": "Accounts", "link_to": "Accounts",

View File

@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document):
def populate_payment_entries(self): def populate_payment_entries(self):
if self.bank_statement is None: return if self.bank_statement is None: return
filename = self.bank_statement.split("/")[-1] file_url = self.bank_statement
if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
frappe.throw(_("Transactions already retreived from the statement")) frappe.throw(_("Transactions already retreived from the statement"))
@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document):
if self.bank_settings: if self.bank_settings:
mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
statement_headers = self.get_statement_headers() statement_headers = self.get_statement_headers()
transactions = get_transaction_entries(filename, statement_headers) transactions = get_transaction_entries(file_url, statement_headers)
for entry in transactions: for entry in transactions:
date = entry[statement_headers["Date"]].strip() date = entry[statement_headers["Date"]].strip()
#print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row):
transaction[header] = "" transaction[header] = ""
return transaction return transaction
def get_transaction_entries(filename, headers): def get_transaction_entries(file_url, headers):
header_index = {} header_index = {}
rows, transactions = [], [] rows, transactions = [], []
if (filename.lower().endswith("xlsx")): if (file_url.lower().endswith("xlsx")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(file_id=filename) rows = read_xlsx_file_from_attached_file(file_url=file_url)
elif (filename.lower().endswith("csv")): elif (file_url.lower().endswith("csv")):
from frappe.utils.csvutils import read_csv_content from frappe.utils.csvutils import read_csv_content
_file = frappe.get_doc("File", {"file_name": filename}) _file = frappe.get_doc("File", {"file_url": file_url})
filepath = _file.get_full_path() filepath = _file.get_full_path()
with open(filepath,'rb') as csvfile: with open(filepath,'rb') as csvfile:
rows = read_csv_content(csvfile.read()) rows = read_csv_content(csvfile.read())
elif (filename.lower().endswith("xls")): elif (file_url.lower().endswith("xls")):
filename = file_url.split("/")[-1]
rows = get_rows_from_xls_file(filename) rows = get_rows_from_xls_file(filename)
else: else:
frappe.throw(_("Only .csv and .xlsx files are supported currently")) frappe.throw(_("Only .csv and .xlsx files are supported currently"))

View File

@ -91,15 +91,11 @@ class TestBankTransaction(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
def add_transactions(): def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank", "doctype": "Bank",
"bank_name":"Citi Bank", "bank_name":bank_name,
}).insert() }).insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -108,12 +104,19 @@ def add_transactions():
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank Account", "doctype": "Bank Account",
"account_name":"Checking Account", "account_name":"Checking Account",
"bank": "Citi Bank", "bank": bank_name,
"account": "_Test Bank - _TC" "account": account_name
}).insert() }).insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",

View File

@ -38,8 +38,8 @@
"reqd": 1 "reqd": 1
} }
], ],
"modified": "2020-06-18 20:27:42.615842", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "ahmad@havenir.com", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",
"owner": "Administrator", "owner": "Administrator",

View File

@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, {
return { filters: filters }; return { filters: filters };
}, },
reverse_journal_entry: function(frm) { reverse_journal_entry: function() {
var me = frm.doc; frappe.model.open_mapped_doc({
for(var i=0; i<me.accounts.length; i++) { method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry",
me.accounts[i].credit += me.accounts[i].debit; frm: cur_frm
me.accounts[i].debit = me.accounts[i].credit - me.accounts[i].debit; })
me.accounts[i].credit -= me.accounts[i].debit; },
me.accounts[i].credit_in_account_currency = me.accounts[i].credit;
me.accounts[i].debit_in_account_currency = me.accounts[i].debit;
me.accounts[i].reference_type = "Journal Entry";
me.accounts[i].reference_name = me.name
}
frm.copy_doc();
cur_frm.reload_doc();
}
}); });
$.extend(erpnext.journal_entry, { $.extend(erpnext.journal_entry, {

View File

@ -1021,3 +1021,34 @@ def make_inter_company_journal_entry(name, voucher_type, company):
journal_entry.posting_date = nowdate() journal_entry.posting_date = nowdate()
journal_entry.inter_company_journal_entry_reference = name journal_entry.inter_company_journal_entry_reference = name
return journal_entry.as_dict() return journal_entry.as_dict()
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
"doctype": "Journal Entry",
"validation": {
"docstatus": ["=", 1]
}
},
"Journal Entry Account": {
"doctype": "Journal Entry Account",
"field_map": {
"account_currency": "account_currency",
"exchange_rate": "exchange_rate",
"debit_in_account_currency": "credit_in_account_currency",
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
},
}, target_doc)
return doclist

View File

@ -167,6 +167,49 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1
jv.submit()
rjv = make_reverse_journal_entry(jv.name)
rjv.posting_date = nowdate()
rjv.submit()
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", rjv.name, as_dict=1)
self.assertTrue(gl_entries)
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
"Sales - _TC": {
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
}
}
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self): def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD # create jv in USD
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC",

View File

@ -45,11 +45,11 @@
], ],
"icon": "fa fa-credit-card", "icon": "fa fa-credit-card",
"idx": 1, "idx": 1,
"modified": "2019-08-14 14:58:42.079115", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "sammish.thundiyil@gmail.com", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Mode of Payment", "name": "Mode of Payment",
"owner": "harshada@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"create": 1, "create": 1,

View File

@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', {
setup: function(frm) { setup: function(frm) {
frm.set_query("paid_from", function() { frm.set_query("paid_from", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return { return {
filters: { filters: {
"account_type": ["in", account_types], "account_type": ["in", account_types],
@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', {
} }
} }
}); });
frm.set_query("party_type", function() { frm.set_query("party_type", function() {
frm.events.validate_company(frm);
return{ return{
filters: { filters: {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "name": ["in", Object.keys(frappe.boot.party_account_types)],
} }
} }
}); });
frm.set_query("party_bank_account", function() { frm.set_query("party_bank_account", function() {
return { return {
filters: { filters: {
@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', {
} }
} }
}); });
frm.set_query("bank_account", function() { frm.set_query("bank_account", function() {
return { return {
filters: { filters: {
@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', {
} }
} }
}); });
frm.set_query("contact_person", function() { frm.set_query("contact_person", function() {
if (frm.doc.party) { if (frm.doc.party) {
return { return {
@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', {
}; };
} }
}); });
frm.set_query("paid_to", function() { frm.set_query("paid_to", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return { return {
filters: { filters: {
"account_type": ["in", account_types], "account_type": ["in", account_types],
@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', {
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
}, },
validate_company: (frm) => {
if (!frm.doc.company){
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
}
},
company: function(frm) { company: function(frm) {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597", "creation": "2016-06-01 14:38:51.012597",
@ -63,6 +64,7 @@
"cost_center", "cost_center",
"section_break_12", "section_break_12",
"status", "status",
"custom_remarks",
"remarks", "remarks",
"column_break_16", "column_break_16",
"letter_head", "letter_head",
@ -462,7 +464,8 @@
"fieldname": "remarks", "fieldname": "remarks",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Remarks", "label": "Remarks",
"no_copy": 1 "no_copy": 1,
"read_only_depends_on": "eval:doc.custom_remarks == 0"
}, },
{ {
"fieldname": "column_break_16", "fieldname": "column_break_16",
@ -573,10 +576,18 @@
"label": "Status", "label": "Status",
"options": "\nDraft\nSubmitted\nCancelled", "options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "custom_remarks",
"fieldtype": "Check",
"label": "Custom Remarks"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-12-08 13:02:30.016610", "links": [],
"modified": "2020-09-02 13:39:43.383705",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -453,7 +453,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self): def set_remarks(self):
if self.remarks: return if self.custom_remarks: return
if self.payment_type=="Internal Transfer": if self.payment_type=="Internal Transfer":
remarks = [_("Amount {0} {1} transferred from {2} to {3}") remarks = [_("Amount {0} {1} transferred from {2} to {3}")
@ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target): def set_missing_values(source, target):
target.payment_order_type = "Payment Entry" target.payment_order_type = "Payment Entry"
target.append('references', dict(
def update_item(source_doc, target_doc, source_parent): reference_doctype="Payment Entry",
target_doc.bank_account = source_parent.party_bank_account reference_name=source.name,
target_doc.amount = source_doc.allocated_amount bank_account=source.party_bank_account,
target_doc.account = source_parent.paid_to amount=source.paid_amount,
target_doc.payment_entry = source_parent.name account=source.paid_to,
target_doc.supplier = source_parent.party supplier=source.party,
target_doc.mode_of_payment = source_parent.mode_of_payment mode_of_payment=source.mode_of_payment,
))
doclist = get_mapped_doc("Payment Entry", source_name, { doclist = get_mapped_doc("Payment Entry", source_name, {
"Payment Entry": { "Payment Entry": {
"doctype": "Payment Order", "doctype": "Payment Order",
"validation": { "validation": {
"docstatus": ["=", 1] "docstatus": ["=", 1]
},
} }
},
"Payment Entry Reference": {
"doctype": "Payment Order Reference",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_item
},
}, target_doc, set_missing_values) }, target_doc, set_missing_values)

View File

@ -21,10 +21,15 @@ class PaymentOrder(Document):
if cancel: if cancel:
status = 'Initiated' status = 'Initiated'
ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status" if self.payment_order_type == "Payment Request":
ref_field = "status"
ref_doc_field = frappe.scrub(self.payment_order_type)
else:
ref_field = "payment_order_status"
ref_doc_field = "reference_name"
for d in self.references: for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -5,6 +5,45 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(unittest.TestCase): class TestPaymentOrder(unittest.TestCase):
pass def setUp(self):
create_bank_account()
def tearDown(self):
for bt in frappe.get_all("Payment Order"):
doc = frappe.get_doc("Payment Order", bt.name)
doc.cancel()
doc.delete()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC")
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
payment_entry.insert()
payment_entry.submit()
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
reference_doc = doc.get("references")[0]
self.assertEquals(reference_doc.reference_name, payment_entry.name)
self.assertEquals(reference_doc.reference_doctype, "Payment Entry")
self.assertEquals(reference_doc.supplier, "_Test Supplier")
self.assertEquals(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account="Checking Account - Citi Bank"
))
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()
doc.submit()
return doc

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2018-07-20 16:38:06.630813", "creation": "2018-07-20 16:38:06.630813",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -10,7 +11,6 @@
"column_break_4", "column_break_4",
"supplier", "supplier",
"payment_request", "payment_request",
"payment_entry",
"mode_of_payment", "mode_of_payment",
"bank_account_details", "bank_account_details",
"bank_account", "bank_account",
@ -103,17 +103,12 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "payment_entry",
"fieldtype": "Link",
"label": "Payment Entry",
"options": "Payment Entry",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-05-08 13:56:25.724557", "links": [],
"modified": "2020-09-04 08:29:51.014390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Order Reference", "name": "Payment Order Reference",

View File

@ -291,11 +291,11 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:49.089450", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Period Closing Voucher", "name": "Period Closing Voucher",
"owner": "jai@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 1, "amend": 1,

View File

@ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', {
}, },
callback: (r) => { callback: (r) => {
let pos_docs = r.message; let pos_docs = r.message;
set_form_data(pos_docs, frm) set_form_data(pos_docs, frm);
refresh_fields(frm) refresh_fields(frm);
set_html_data(frm) set_html_data(frm);
} }
}) })
} }
}); });
cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) {
const removed_row = locals[cdt][cdn];
if (!removed_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => {
cur_frm.doc.grand_total -= flt(doc.grand_total);
cur_frm.doc.net_total -= flt(doc.net_total);
cur_frm.doc.total_quantity -= flt(doc.total_qty);
refresh_payments(doc, cur_frm, 1);
refresh_taxes(doc, cur_frm, 1);
refresh_fields(cur_frm);
set_html_data(cur_frm);
});
}
frappe.ui.form.on('POS Invoice Reference', {
pos_invoice(frm, cdt, cdn) {
const added_row = locals[cdt][cdn];
if (!added_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
}
})
frappe.ui.form.on('POS Closing Entry Detail', { frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => { closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -76,8 +110,8 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total); frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total); frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty); frm.doc.total_quantity += flt(d.total_qty);
add_to_payments(d, frm); refresh_payments(d, frm);
add_to_taxes(d, frm); refresh_taxes(d, frm);
}); });
} }
@ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) {
}) })
} }
function add_to_payments(d, frm) { function refresh_payments(d, frm, remove) {
d.payments.forEach(p => { d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (payment) { if (payment) {
payment.expected_amount += flt(p.amount); if (!remove) payment.expected_amount += flt(p.amount);
else payment.expected_amount -= flt(p.amount);
} else { } else {
frm.add_child("payment_reconciliation", { frm.add_child("payment_reconciliation", {
mode_of_payment: p.mode_of_payment, mode_of_payment: p.mode_of_payment,
@ -105,11 +140,12 @@ function add_to_payments(d, frm) {
}) })
} }
function add_to_taxes(d, frm) { function refresh_taxes(d, frm, remove) {
d.taxes.forEach(t => { d.taxes.forEach(t => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) { if (tax) {
tax.amount += flt(t.tax_amount); if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
} else { } else {
frm.add_child("taxes", { frm.add_child("taxes", {
account_head: t.account_head, account_head: t.account_head,

View File

@ -279,7 +279,8 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Return (Credit Note)", "label": "Is Return (Credit Note)",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"set_only_once": 1
}, },
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
@ -1578,9 +1579,10 @@
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:39.337385", "modified": "2020-09-07 12:43:09.138720",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase):
def test_pos_returns_with_repayment(self): def test_pos_returns_with_repayment(self):
pos = create_pos_invoice(qty = 10, do_not_save=True) pos = create_pos_invoice(qty = 10, do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()
@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase):
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
cost_center = "Main - _TC", do_not_save=True) cost_center = "Main - _TC", do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()

View File

@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document):
def validate_pos_invoice_status(self): def validate_pos_invoice_status(self):
for d in self.pos_invoices: for d in self.pos_invoices:
status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
if docstatus != 1: if docstatus != 1:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
if status in ['Consolidated']: if status == "Consolidated":
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
frappe.throw(
_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
You can add original invoice {} manually to proceed.")
.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
)
def on_submit(self): def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
@ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document):
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales) sales_invoice = self.process_merging_into_sales_invoice(sales)
if len(returns): if returns:
credit_note = self.process_merging_into_credit_note(returns) credit_note = self.process_merging_into_credit_note(returns)
else:
credit_note = ""
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log

View File

@ -5,13 +5,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater): class POSOpeningEntry(StatusUpdater):
def validate(self): def validate(self):
self.validate_pos_profile_and_cashier() self.validate_pos_profile_and_cashier()
self.validate_payment_method_account()
self.set_status() self.set_status()
def validate_pos_profile_and_cashier(self): def validate_pos_profile_and_cashier(self):
@ -21,5 +22,13 @@ class POSOpeningEntry(StatusUpdater):
if not cint(frappe.db.get_value("User", self.user, "enabled")): if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
def validate_payment_method_account(self):
for d in self.balance_details:
account = frappe.db.get_value("Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
def on_submit(self): def on_submit(self):
self.set_status(update=True) self.set_status(update=True)

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import msgprint, _ from frappe import msgprint, _
from frappe.utils import cint, now from frappe.utils import cint, now, get_link_to_form
from six import iteritems from six import iteritems
from frappe.model.document import Document from frappe.model.document import Document
@ -13,7 +13,7 @@ class POSProfile(Document):
self.validate_default_profile() self.validate_default_profile()
self.validate_all_link_fields() self.validate_all_link_fields()
self.validate_duplicate_groups() self.validate_duplicate_groups()
self.check_default_payment() self.validate_payment_methods()
def validate_default_profile(self): def validate_default_profile(self):
for row in self.applicable_for_users: for row in self.applicable_for_users:
@ -52,14 +52,23 @@ class POSProfile(Document):
if len(customer_groups) != len(set(customer_groups)): if len(customer_groups) != len(set(customer_groups)):
frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group")
def check_default_payment(self): def validate_payment_methods(self):
if self.payments: if not self.payments:
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
default_mode_of_payment = [d.default for d in self.payments if d.default] default_mode_of_payment = [d.default for d in self.payments if d.default]
if not default_mode_of_payment: if not default_mode_of_payment:
frappe.throw(_("Set default mode of payment")) frappe.throw(_("Please select a default mode of payment"))
if len(default_mode_of_payment) > 1: if len(default_mode_of_payment) > 1:
frappe.throw(_("Multiple default mode of payment is not allowed")) frappe.throw(_("You can only select one mode of payment as default"))
for d in self.payments:
account = frappe.db.get_value("Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
def on_update(self): def on_update(self):
self.set_defaults() self.set_defaults()

View File

@ -7,10 +7,10 @@ frappe.ui.form.on('POS Settings', {
}, },
get_invoice_fields: function(frm) { get_invoice_fields: function(frm) {
frappe.model.with_doctype("Sales Invoice", () => { frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
d.fieldtype === 'Table') { ['Table', 'Button'].includes(d.fieldtype)) {
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
} else { } else {
return null; return null;
@ -25,7 +25,7 @@ frappe.ui.form.on('POS Settings', {
frappe.ui.form.on("POS Field", { frappe.ui.form.on("POS Field", {
fieldname: function(frm, doctype, name) { fieldname: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name); var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
return doc.fieldname == d.fieldname ? d : null; return doc.fieldname == d.fieldname ? d : null;
})[0]; })[0];

View File

@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', {
} }
}; };
}); });
},
if (frm.doc.company) { type: function(frm) {
if (frm.doc.company && frm.doc.type) {
frm.set_query("account", function() { frm.set_query("account", function() {
return { return {
filters: { filters: {
'company': frm.doc.company, 'company': frm.doc.company,
'root_type': 'Liability', 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset',
'is_group': 0 'is_group': 0
} }
}; };

View File

@ -60,6 +60,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval: doc.type",
"fieldname": "account", "fieldname": "account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Account", "label": "Account",
@ -73,9 +74,10 @@
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-02-06 18:18:09.852844", "modified": "2020-09-03 18:07:02.463754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Deferred Accounting", "name": "Process Deferred Accounting",

View File

@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this.frm.set_df_property("credit_to", "print_hide", 0); this.frm.set_df_property("credit_to", "print_hide", 0);
} }
} }
// Trigger supplier event on load if supplier is available
// The reason for this is PI can be created from PR or PO and supplier is pre populated
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
}, },
refresh: function(doc) { refresh: function(doc) {
@ -135,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
} }
}); });
} }
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
}, },
unblock_invoice: function() { unblock_invoice: function() {

View File

@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController):
if not self.due_date: if not self.due_date:
self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.apply_tds = 1
self.tax_withholding_category = tds_category
super(PurchaseInvoice, self).set_missing_values(for_validate) super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self): def check_conversion_rate(self):
@ -400,8 +405,6 @@ class PurchaseInvoice(BuyingController):
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
def make_gl_entries(self, gl_entries=None): def make_gl_entries(self, gl_entries=None):
if not self.grand_total:
return
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()

View File

@ -210,7 +210,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-12 14:53:47.679439", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges", "name": "Purchase Taxes and Charges",

View File

@ -78,7 +78,7 @@
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges Template", "name": "Purchase Taxes and Charges Template",
"owner": "wasim@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"email": 1, "email": 1,

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@ -1372,7 +1372,7 @@ def get_bank_cash_account(mode_of_payment, company):
{"parent": mode_of_payment, "company": company}, "default_account") {"parent": mode_of_payment, "company": company}, "default_account")
if not account: if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(mode_of_payment)) .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
return { return {
"account": account "account": account
} }
@ -1612,16 +1612,14 @@ def update_multi_mode_option(doc, pos_profile):
payment.type = payment_mode.type payment.type = payment_mode.type
doc.set('payments', []) doc.set('payments', [])
if not pos_profile or not pos_profile.get('payments'):
for payment_mode in get_all_mode_of_payments(doc):
append_payment(payment_mode)
return
for pos_payment_method in pos_profile.get('payments'): for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict() pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
if payment_mode: if not payment_mode:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account"))
payment_mode[0].default = pos_payment_method.default payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0]) append_payment(payment_mode[0])

View File

@ -1,134 +1,66 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "allow_rename": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:title", "autoname": "field:title",
"beta": 0,
"creation": "2018-11-22 23:38:39.668804", "creation": "2018-11-22 23:38:39.668804",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"title"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"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": "Title", "label": "Title",
"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": 1 "unique": 1
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-08-30 19:41:25.783852",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-01-15 17:14:28.951793",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Category", "name": "Tax Category",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts Manager", "role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts User", "role": "Accounts User",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -45,8 +45,8 @@ def validate_accounting_period(gl_map):
}, as_dict=1) }, as_dict=1)
if accounting_periods: if accounting_periods:
frappe.throw(_("You can't create accounting entries in the closed accounting period {0}") frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
.format(accounting_periods[0].name), ClosedAccountingPeriod) .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True): def process_gl_map(gl_map, merge_entries=True):
if merge_entries: if merge_entries:
@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
}) })
if gl_entries: if gl_entries:
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj) check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
for entry in gl_entries: for entry in gl_entries:
entry['name'] = None entry['name'] = None
@ -319,7 +320,6 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
entry['remarks'] = "On cancellation of " + entry['voucher_no'] entry['remarks'] = "On cancellation of " + entry['voucher_no']
entry['is_cancelled'] = 1 entry['is_cancelled'] = 1
entry['posting_date'] = today()
if entry['debit'] or entry['credit']: if entry['debit'] or entry['credit']:
make_entry(entry, adv_adj, "Yes") make_entry(entry, adv_adj, "Yes")
@ -342,7 +342,7 @@ def set_as_cancel(voucher_type, voucher_no):
""" """
Set is_cancelled=1 in all original gl entries for the voucher Set is_cancelled=1 in all original gl entries for the voucher
""" """
frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s modified=%s, modified_by=%s
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no)) (now(), frappe.session.user, voucher_type, voucher_no))

View File

@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() { check_plaid_status() {
const me = this; const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r && r.enabled == "1") { if (r && r.enabled === "1") {
me.plaid_status = "active" me.plaid_status = "active"
} else { } else {
me.plaid_status = "inactive" me.plaid_status = "inactive"
@ -214,29 +214,33 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() { init_config() {
const me = this; const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
.then(result => { .then(result => {
me.plaid_env = result.plaid_env; me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key;
me.client_name = result.client_name; me.client_name = result.client_name;
me.sync_transactions() me.link_token = result.link_token;
me.sync_transactions();
}) })
} }
sync_transactions() { sync_transactions() {
const me = this; const me = this;
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
bank: v['bank'], bank: r.bank,
bank_account: me.parent.bank_account, bank_account: me.parent.bank_account,
freeze: true freeze: true
}) })
.then((result) => { .then((result) => {
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") let result_title = (result && result.length > 0)
? __("{0} bank transaction(s) created", [result.length])
: __("This bank account is already synchronized");
let result_msg = ` let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;"> <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5> <h5 class="text-muted">${result_title}</h5>
</div>` </div>`
this.parent.$main_section.append(result_msg) this.parent.$main_section.append(result_msg)
frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
}) })

View File

@ -34,7 +34,7 @@ frappe.query_reports["Accounts Receivable"] = {
filters: { filters: {
'company': company 'company': company
} }
} };
} }
}, },
{ {

View File

@ -617,9 +617,19 @@ class ReceivablePayableReport(object):
elif party_type_field=="supplier": elif party_type_field=="supplier":
self.add_supplier_filters(conditions, values) self.add_supplier_filters(conditions, values)
if self.filters.cost_center:
self.get_cost_center_conditions(conditions)
self.add_accounting_dimensions_filters(conditions, values) self.add_accounting_dimensions_filters(conditions, values)
return " and ".join(conditions), values return " and ".join(conditions), values
def get_cost_center_conditions(self, conditions):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})]
cost_center_string = '", "'.join(cost_center_list)
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
def get_order_by_condition(self): def get_order_by_condition(self):
if self.filters.get('group_by_party'): if self.filters.get('group_by_party'):
return "order by party, posting_date" return "order by party, posting_date"

View File

@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check", fieldtype: "Check",
default: 0, default: 0,
}, },
] ],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes('variance')) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
} }
erpnext.dimension_filters.forEach((dimension) => { erpnext.dimension_filters.forEach((dimension) => {

View File

@ -173,7 +173,7 @@ class PartyLedgerSummaryReport(object):
from `tabGL Entry` gle from `tabGL Entry` gle
{join} {join}
where where
gle.docstatus < 2 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != ''
and gle.posting_date <= %(to_date)s {conditions} and gle.posting_date <= %(to_date)s {conditions}
order by gle.posting_date order by gle.posting_date
""".format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True)
@ -248,7 +248,7 @@ class PartyLedgerSummaryReport(object):
from from
`tabGL Entry` `tabGL Entry`
where where
docstatus < 2 docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in ( and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
where acc.name = gle.account and acc.account_type = '{income_or_expense}' where acc.name = gle.account and acc.account_type = '{income_or_expense}'

View File

@ -43,8 +43,11 @@ def execute(filters=None):
def validate_filters(filters, account_details): def validate_filters(filters, account_details):
if not filters.get('company'): if not filters.get("company"):
frappe.throw(_('{0} is mandatory').format(_('Company'))) frappe.throw(_("{0} is mandatory").format(_("Company")))
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
if filters.get("account") and not account_details.get(filters.account): if filters.get("account") and not account_details.get(filters.account):
frappe.throw(_("Account {0} does not exists").format(filters.account)) frappe.throw(_("Account {0} does not exists").format(filters.account))

View File

@ -1,13 +1,12 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-02-25 17:03:34", "creation": "2013-02-25 17:03:34",
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2017-02-24 20:12:22.464240", "modified": "2020-08-13 11:26:39.112352",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Gross Profit", "name": "Gross Profit",

View File

@ -268,9 +268,9 @@ class GrossProfitGenerator(object):
def get_last_purchase_rate(self, item_code, row): def get_last_purchase_rate(self, item_code, row):
condition = '' condition = ''
if row.project: if row.project:
condition += " AND a.project='%s'" % (row.project) condition += " AND a.project=%s" % (frappe.db.escape(row.project))
elif row.cost_center: elif row.cost_center:
condition += " AND a.cost_center='%s'" % (row.cost_center) condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
if self.filters.to_date: if self.filters.to_date:
condition += " AND modified='%s'" % (self.filters.to_date) condition += " AND modified='%s'" % (self.filters.to_date)

View File

@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', {
}) })
}, },
available_for_use_date: function(frm) {
$.each(frm.doc.finance_books || [], function(i, d) {
if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date;
});
refresh_field("finance_books");
},
is_existing_asset: function(frm) { is_existing_asset: function(frm) {
frm.trigger("toggle_reference_doc"); frm.trigger("toggle_reference_doc");
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
@ -438,6 +431,15 @@ frappe.ui.form.on('Asset Finance Book', {
} }
frappe.flags.dont_change_rate = false; frappe.flags.dont_change_rate = false;
},
depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn];
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`));
book.depreciation_start_date = "";
frm.refresh_field("finance_books");
}
} }
}); });

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext, math, json import frappe, erpnext, math, json
from frappe import _ from frappe import _
from six import string_types from six import string_types
from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day, get_datetime
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \ from erpnext.assets.doctype.asset.depreciation \
@ -83,6 +83,11 @@ class Asset(AccountsController):
if not self.available_for_use_date: if not self.available_for_use_date:
frappe.throw(_("Available for use date is required")) frappe.throw(_("Available for use date is required"))
for d in self.finance_books:
if d.depreciation_start_date == self.available_for_use_date:
frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx),
title=_("Incorrect Date"))
def set_missing_values(self): def set_missing_values(self):
if not self.asset_category: if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
@ -135,6 +140,10 @@ class Asset(AccountsController):
def make_asset_movement(self): def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice reference_docname = self.purchase_receipt or self.purchase_invoice
transaction_date = getdate(self.purchase_date)
if reference_docname:
posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"])
transaction_date = get_datetime("{} {}".format(posting_date, posting_time))
assets = [{ assets = [{
'asset': self.name, 'asset': self.name,
'asset_name': self.asset_name, 'asset_name': self.asset_name,
@ -146,7 +155,7 @@ class Asset(AccountsController):
'assets': assets, 'assets': assets,
'purpose': 'Receipt', 'purpose': 'Receipt',
'company': self.company, 'company': self.company,
'transaction_date': getdate(nowdate()), 'transaction_date': transaction_date,
'reference_doctype': reference_doctype, 'reference_doctype': reference_doctype,
'reference_name': reference_docname 'reference_name': reference_docname
}).insert() }).insert()
@ -294,7 +303,7 @@ class Asset(AccountsController):
if not row.depreciation_start_date: if not row.depreciation_start_date:
if not self.available_for_use_date: if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
row.depreciation_start_date = self.available_for_use_date row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset: if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0

View File

@ -371,19 +371,18 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = nowdate() asset.available_for_use_date = '2020-01-01'
asset.purchase_date = nowdate() asset.purchase_date = '2020-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 10,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 1
"depreciation_start_date": nowdate()
}) })
asset.insert() asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date=add_months(nowdate(), 10)) post_depreciation_entries(date=add_months('2020-01-01', 4))
scrap_asset(asset.name) scrap_asset(asset.name)
@ -392,9 +391,9 @@ class TestAsset(unittest.TestCase):
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@ -466,8 +465,7 @@ class TestAsset(unittest.TestCase):
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
asset.insert() asset.insert()
accumulated_depreciation_after_full_schedule = \ accumulated_depreciation_after_full_schedule = \

View File

@ -1,347 +1,99 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-05-08 14:44:37.095570", "creation": "2018-05-08 14:44:37.095570",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
"expected_value_after_useful_life",
"value_after_depreciation",
"rate_of_depreciation"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Finance Book", "label": "Finance Book",
"length": 0, "options": "Finance Book"
"no_copy": 0,
"options": "Finance Book",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "depreciation_method", "fieldname": "depreciation_method",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Depreciation Method", "label": "Depreciation Method",
"length": 0,
"no_copy": 0,
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "total_number_of_depreciations", "fieldname": "total_number_of_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Number of Depreciations", "label": "Total Number of Depreciations",
"length": 0, "reqd": 1
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "frequency_of_depreciation", "fieldname": "frequency_of_depreciation",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Frequency of Depreciation (Months)", "label": "Frequency of Depreciation (Months)",
"length": 0, "reqd": 1
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:parent.doctype == 'Asset'", "depends_on": "eval:parent.doctype == 'Asset'",
"fetch_if_empty": 0,
"fieldname": "depreciation_start_date", "fieldname": "depreciation_start_date",
"fieldtype": "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_list_view": 1,
"in_standard_filter": 0, "label": "Depreciation Posting Date",
"label": "Depreciation Start Date", "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"depends_on": "eval:parent.doctype == 'Asset'", "depends_on": "eval:parent.doctype == 'Asset'",
"fetch_if_empty": 0,
"fieldname": "expected_value_after_useful_life", "fieldname": "expected_value_after_useful_life",
"fieldtype": "Currency", "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": "Expected Value After Useful Life", "label": "Expected Value After Useful Life",
"length": 0, "options": "Company:company:default_currency"
"no_copy": 0,
"options": "Company:company:default_currency",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "value_after_depreciation", "fieldname": "value_after_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Value After Depreciation", "label": "Value After Depreciation",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'", "depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"description": "In Percentage", "description": "In Percentage",
"fetch_if_empty": 0,
"fieldname": "rate_of_depreciation", "fieldname": "rate_of_depreciation",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Rate of Depreciation"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Depreciation",
"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
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-04-09 19:45:14.523488", "modified": "2020-09-16 12:11:30.631788",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31", "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
if asset.docstatus == 0: if asset.docstatus == 0:
@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31", "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
if asset.docstatus == 0: if asset.docstatus == 0:
asset.submit() asset.submit()

View File

@ -1084,7 +1084,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-31 14:13:44.610190", "modified": "2020-09-14 14:36:12.418690",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",
@ -1135,5 +1135,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "supplier", "timeline_field": "supplier",
"title_field": "supplier" "title_field": "supplier",
"track_changes": 1
} }

View File

@ -89,7 +89,7 @@ class TestPurchaseOrder(unittest.TestCase):
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
def test_update_child_qty_rate(self): def test_update_child(self):
mr = make_material_request(qty=10) mr = make_material_request(qty=10)
po = make_purchase_order(mr.name) po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier" po.supplier = "_Test Supplier"
@ -119,7 +119,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
def test_add_new_item_in_update_child_qty_rate(self): def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4 po.items[0].qty = 4
po.save() po.save()
@ -145,7 +145,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
def test_remove_item_in_update_child_qty_rate(self): def test_update_child_removing_item(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4 po.items[0].qty = 4
po.save() po.save()
@ -185,7 +185,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEquals(len(po.get('items')), 1) self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
def test_update_child_qty_rate_perm(self): def test_update_child_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4) po = create_purchase_order(item_code= "_Test Item", qty=4)
user = 'test@example.com' user = 'test@example.com'
@ -202,6 +202,72 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
frappe.set_user("Administrator") frappe.set_user("Administrator")
def test_update_child_with_tax_template(self):
tax_template = "_Test Account Excise Duty @ 10"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
item_doc.append("taxes", {
"item_tax_template": tax_template,
"valid_from": nowdate()
})
item_doc.save()
else:
# update valid from
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template})
po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
po.append("taxes", {
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Purchase Taxes and Charges",
"rate": 10
})
po.insert()
po.submit()
self.assertEqual(po.taxes[0].tax_amount, 50)
self.assertEqual(po.taxes[0].total, 550)
items = json.dumps([
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
{'item_code' : item, 'rate' : 100, 'qty' : 1} # added item
])
update_child_qty_rate('Purchase Order', items, po.name)
po.reload()
self.assertEqual(po.taxes[0].tax_amount, 60)
self.assertEqual(po.taxes[0].total, 660)
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template})
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
trans_item = json.dumps([{
'item_code': po.get("items")[0].item_code,
'rate': po.get("items")[0].rate,
'qty': po.get("items")[0].qty,
'uom': "_Test UOM 1",
'conversion_factor': 2,
'docname': po.get("items")[0].name
}])
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
def test_update_qty(self): def test_update_qty(self):
po = create_purchase_order() po = create_purchase_order()

View File

@ -148,11 +148,11 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-12 15:43:53.862897", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item Supplied", "name": "Purchase Order Item Supplied",
"owner": "dhanalekshmi@webnotestech.com", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC"

View File

@ -188,11 +188,11 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-10 18:09:33.997618", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Receipt Item Supplied", "name": "Purchase Receipt Item Supplied",
"owner": "wasim@webnotestech.com", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",

View File

@ -11,6 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.templates.pages.rfq import check_supplier_has_docname_access from erpnext.templates.pages.rfq import check_supplier_has_docname_access
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
class TestRequestforQuotation(unittest.TestCase): class TestRequestforQuotation(unittest.TestCase):
def test_quote_status(self): def test_quote_status(self):
@ -110,6 +112,23 @@ class TestRequestforQuotation(unittest.TestCase):
self.assertEqual(supplier_quotation.items[0].qty, 5) self.assertEqual(supplier_quotation.items[0].qty, 5)
self.assertEqual(supplier_quotation.items[0].stock_qty, 10) self.assertEqual(supplier_quotation.items[0].stock_qty, 10)
def test_make_rfq_from_opportunity(self):
opportunity = make_opportunity(with_items=1)
supplier_data = get_supplier_data()
rfq = make_rfq(opportunity.name)
self.assertEqual(len(rfq.get("items")), len(opportunity.get("items")))
rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.'
for item in rfq.items:
item.warehouse = "_Test Warehouse - _TC"
for data in supplier_data:
rfq.append('suppliers', data)
rfq.status = 'Draft'
rfq.submit()
def make_request_for_quotation(**args): def make_request_for_quotation(**args):
""" """
:param supplier_data: List containing supplier data :param supplier_data: List containing supplier data

View File

@ -178,6 +178,7 @@ def make_all_scorecards(docname):
period_card = make_supplier_scorecard(docname, None) period_card = make_supplier_scorecard(docname, None)
period_card.start_date = start_date period_card.start_date = start_date
period_card.end_date = end_date period_card.end_date = end_date
period_card.insert(ignore_permissions=True)
period_card.submit() period_card.submit()
scp_count = scp_count + 1 scp_count = scp_count + 1
if start_date < first_start_date: if start_date < first_start_date:

View File

@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None):
"doctype": "Supplier Scorecard Scoring Criteria", "doctype": "Supplier Scorecard Scoring Criteria",
"postprocess": update_criteria_fields, "postprocess": update_criteria_fields,
} }
}, target_doc, post_process) }, target_doc, post_process, ignore_permissions=True)
return doc return doc

View File

@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = {
"reqd": 1 "reqd": 1
}, },
{ {
reqd: 1, "fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"width": "80",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"width": "80",
"reqd": 1,
"default": frappe.datetime.get_today()
},
{
default: "", default: "",
options: "Item", options: "Item",
label: __("Item"), label: __("Item"),
@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = {
} }
}, },
{ {
fieldtype: "Link", fieldtype: "MultiSelectList",
label: __("Supplier Quotation"), label: __("Supplier Quotation"),
options: "Supplier Quotation",
fieldname: "supplier_quotation", fieldname: "supplier_quotation",
default: "", default: "",
get_query: () => { get_data: function(txt) {
return { filters: { "docstatus": ["<", 2] } } return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]});
} }
}, },
{ {
@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = {
get_query: () => { get_query: () => {
return { filters: { "docstatus": ["<", 2] } } return { filters: { "docstatus": ["<", 2] } }
} }
},
{
fieldtype: "Check",
label: __("Include Expired"),
fieldname: "include_expired",
default: 0
} }
], ],
formatter: (value, row, column, data, default_formatter) => {
value = default_formatter(value, row, column, data);
if(column.fieldname === "valid_till" && data.valid_till){
if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){
value = `<div style="color:red">${value}</div>`;
}
else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){
value = `<div style="color:darkorange">${value}</div>`;
}
}
return value;
},
onload: (report) => { onload: (report) => {
// Create a button for setting the default supplier // Create a button for setting the default supplier
report.page.add_inner_button(__("Select Default Supplier"), () => { report.page.add_inner_button(__("Select Default Supplier"), () => {

View File

@ -16,44 +16,49 @@ def execute(filters=None):
supplier_quotation_data = get_data(filters, conditions) supplier_quotation_data = get_data(filters, conditions)
columns = get_columns() columns = get_columns()
data, chart_data = prepare_data(supplier_quotation_data) data, chart_data = prepare_data(supplier_quotation_data, filters)
message = get_message()
return columns, data, None, chart_data return columns, data, message, chart_data
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
if filters.get("item_code"):
conditions += " AND sqi.item_code = %(item_code)s"
if filters.get("supplier_quotation"): if filters.get("supplier_quotation"):
conditions += " AND sqi.parent = %(supplier_quotation)s" conditions += " AND sqi.parent in %(supplier_quotation)s"
if filters.get("request_for_quotation"): if filters.get("request_for_quotation"):
conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s"
if filters.get("supplier"): if filters.get("supplier"):
conditions += " AND sq.supplier in %(supplier)s" conditions += " AND sq.supplier in %(supplier)s"
if not filters.get("include_expired"):
conditions += " AND sq.status != 'Expired'"
return conditions return conditions
def get_data(filters, conditions): def get_data(filters, conditions):
if not filters.get("item_code"):
return []
supplier_quotation_data = frappe.db.sql("""SELECT supplier_quotation_data = frappe.db.sql("""SELECT
sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation,
sq.supplier sqi.lead_time_days, sq.supplier, sq.valid_till
FROM FROM
`tabSupplier Quotation Item` sqi, `tabSupplier Quotation Item` sqi,
`tabSupplier Quotation` sq `tabSupplier Quotation` sq
WHERE WHERE
sqi.item_code = %(item_code)s sqi.parent = sq.name
AND sqi.parent = sq.name
AND sqi.docstatus < 2 AND sqi.docstatus < 2
AND sq.company = %(company)s AND sq.company = %(company)s
AND sq.status != 'Expired' AND sq.transaction_date between %(from_date)s and %(to_date)s
{0}""".format(conditions), filters, as_dict=1) {0}
order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1)
return supplier_quotation_data return supplier_quotation_data
def prepare_data(supplier_quotation_data): def prepare_data(supplier_quotation_data, filters):
out, suppliers, qty_list = [], [], [] out, suppliers, qty_list, chart_data = [], [], [], []
supplier_wise_map = defaultdict(list) supplier_wise_map = defaultdict(list)
supplier_qty_price_map = {} supplier_qty_price_map = {}
@ -70,17 +75,21 @@ def prepare_data(supplier_quotation_data):
exchange_rate = 1 exchange_rate = 1
row = { row = {
"item_code": data.get('item_code'),
"quotation": data.get("parent"), "quotation": data.get("parent"),
"qty": data.get("qty"), "qty": data.get("qty"),
"price": flt(data.get("rate") * exchange_rate, float_precision), "price": flt(data.get("rate") * exchange_rate, float_precision),
"uom": data.get("uom"), "uom": data.get("uom"),
"request_for_quotation": data.get("request_for_quotation"), "request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get('valid_till'),
"lead_time_days": data.get('lead_time_days')
} }
# map for report view of form {'supplier1':[{},{},...]} # map for report view of form {'supplier1':[{},{},...]}
supplier_wise_map[supplier].append(row) supplier_wise_map[supplier].append(row)
# map for chart preparation of the form {'supplier1': {'qty': 'price'}} # map for chart preparation of the form {'supplier1': {'qty': 'price'}}
if filters.get("item_code"):
if not supplier in supplier_qty_price_map: if not supplier in supplier_qty_price_map:
supplier_qty_price_map[supplier] = {} supplier_qty_price_map[supplier] = {}
supplier_qty_price_map[supplier][row["qty"]] = row["price"] supplier_qty_price_map[supplier][row["qty"]] = row["price"]
@ -97,6 +106,7 @@ def prepare_data(supplier_quotation_data):
for entry in supplier_wise_map[supplier]: for entry in supplier_wise_map[supplier]:
out.append(entry) out.append(entry)
if filters.get("item_code"):
chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
return out, chart_data return out, chart_data
@ -117,9 +127,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
data_points_map[qty].append(None) data_points_map[qty].append(None)
dataset = [] dataset = []
currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol")
for qty in qty_list: for qty in qty_list:
datapoints = { datapoints = {
"name": _("Price for Qty ") + str(qty), "name": currency_symbol + " (Qty " + str(qty) + " )",
"values": data_points_map[qty] "values": data_points_map[qty]
} }
dataset.append(datapoints) dataset.append(datapoints)
@ -140,14 +151,21 @@ def get_columns():
"label": _("Supplier"), "label": _("Supplier"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier", "options": "Supplier",
"width": 150
},
{
"fieldname": "item_code",
"label": _("Item"),
"fieldtype": "Link",
"options": "Item",
"width": 200 "width": 200
}, },
{ {
"fieldname": "quotation", "fieldname": "uom",
"label": _("Supplier Quotation"), "label": _("UOM"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Supplier Quotation", "options": "UOM",
"width": 200 "width": 90
}, },
{ {
"fieldname": "qty", "fieldname": "qty",
@ -163,19 +181,43 @@ def get_columns():
"width": 110 "width": 110
}, },
{ {
"fieldname": "uom", "fieldname": "quotation",
"label": _("UOM"), "label": _("Supplier Quotation"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "UOM", "options": "Supplier Quotation",
"width": 90 "width": 200
},
{
"fieldname": "valid_till",
"label": _("Valid Till"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "lead_time_days",
"label": _("Lead Time (Days)"),
"fieldtype": "Int",
"width": 100
}, },
{ {
"fieldname": "request_for_quotation", "fieldname": "request_for_quotation",
"label": _("Request for Quotation"), "label": _("Request for Quotation"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Request for Quotation", "options": "Request for Quotation",
"width": 200 "width": 150
} }
] ]
return columns return columns
def get_message():
return """<span class="indicator">
Valid till : &nbsp;&nbsp;
</span>
<span class="indicator orange">
Expires in a week or less
</span>
&nbsp;&nbsp;
<span class="indicator red">
Expires today / Already Expired
</span>"""

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "field:id", "autoname": "field:id",
"creation": "2019-06-05 12:07:02.634534", "creation": "2019-06-05 12:07:02.634534",
"doctype": "DocType", "doctype": "DocType",
@ -14,6 +15,7 @@
"contact", "contact",
"contact_name", "contact_name",
"column_break_10", "column_break_10",
"customer",
"lead", "lead",
"lead_name", "lead_name",
"section_break_5", "section_break_5",
@ -28,7 +30,8 @@
}, },
{ {
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Call Details"
}, },
{ {
"fieldname": "id", "fieldname": "id",
@ -125,10 +128,19 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Lead Name", "label": "Lead Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"modified": "2019-08-06 05:46:53.144683", "index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-25 17:08:34.085731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Communication", "module": "Communication",
"name": "Call Log", "name": "Call Log",

View File

@ -16,6 +16,9 @@ class CallLog(Document):
self.contact = get_contact_with_phone_number(number) self.contact = get_contact_with_phone_number(number)
self.lead = get_lead_with_phone_number(number) self.lead = get_lead_with_phone_number(number)
contact = frappe.get_doc("Contact", self.contact)
self.customer = contact.get_link_for("Customer")
def after_insert(self): def after_insert(self):
self.trigger_call_popup() self.trigger_call_popup()

View File

@ -7,6 +7,7 @@ import json
from frappe import _, throw from frappe import _, throw
from frappe.utils import (today, flt, cint, fmt_money, formatdate, from frappe.utils import (today, flt, cint, fmt_money, formatdate,
getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form)
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError
from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.stock.get_item_details import get_conversion_factor, get_item_details
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
@ -19,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from six import text_type from six import text_type
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
from erpnext.stock.get_item_details import get_item_warehouse from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
@ -1157,6 +1158,18 @@ def get_supplier_block_status(party_name):
} }
return info return info
def set_child_tax_template_and_map(item, child_item, parent_doc):
args = {
'item_code': item.item_code,
'posting_date': parent_doc.transaction_date,
'tax_category': parent_doc.get('tax_category'),
'company': parent_doc.get('company')
}
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
""" """
Returns a Sales Order Item child item containing the default values Returns a Sales Order Item child item containing the default values
@ -1168,8 +1181,10 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = trans_item.get("uom") or item.stock_uom
child_item.uom = item.stock_uom conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
set_child_tax_template_and_map(item, child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse: if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@ -1188,13 +1203,15 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = trans_item.get("uom") or item.stock_uom
child_item.uom = item.stock_uom conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
set_child_tax_template_and_map(item, child_item, p_doc)
return child_item return child_item
def check_and_delete_children(parent, data): def validate_and_delete_children(parent, data):
deleted_children = [] deleted_children = []
updated_item_names = [d.get("docname") for d in data] updated_item_names = [d.get("docname") for d in data]
for item in parent.items: for item in parent.items:
@ -1221,18 +1238,40 @@ def check_and_delete_children(parent, data):
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_permissions(doc, perm_type='create'): def check_doc_permissions(doc, perm_type='create'):
try: try:
doc.check_permission(perm_type) doc.check_permission(perm_type)
except: except frappe.PermissionError:
action = "add" if perm_type == 'create' else "update" actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' }
frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions"))
frappe.throw(_("You do not have permissions to {} items in a {}.")
.format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions"))
def validate_workflow_conditions(doc):
workflow = get_workflow_name(doc.doctype)
if not workflow:
return
workflow_doc = frappe.get_doc("Workflow", workflow)
current_state = doc.get(workflow_doc.workflow_state_field)
roles = frappe.get_roles()
transitions = []
for transition in workflow_doc.transitions:
if transition.next_state == current_state and transition.allowed in roles:
if not is_transition_condition_satisfied(transition, doc):
continue
transitions.append(transition.as_dict())
if not transitions:
frappe.throw(
_("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)),
title=_("Insufficient Permissions")
)
def get_new_child_item(item_row): def get_new_child_item(item_row):
if parent_doctype == "Sales Order": new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
if parent_doctype == "Purchase Order":
return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
def validate_quantity(child_item, d): def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
@ -1246,21 +1285,23 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name) parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_and_delete_children(parent, data) check_doc_permissions(parent, 'cancel')
validate_and_delete_children(parent, data)
for d in data: for d in data:
new_child_flag = False new_child_flag = False
if not d.get("docname"): if not d.get("docname"):
new_child_flag = True new_child_flag = True
check_permissions(parent, 'create') check_doc_permissions(parent, 'create')
child_item = get_new_child_item(d) child_item = get_new_child_item(d)
else: else:
check_permissions(parent, 'write') check_doc_permissions(parent, 'write')
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor")) prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor"))
prev_uom, new_uom = child_item.get("uom"), d.get("uom")
if parent_doctype == 'Sales Order': if parent_doctype == 'Sales Order':
prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date") prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date")
@ -1269,9 +1310,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
rate_unchanged = prev_rate == new_rate rate_unchanged = prev_rate == new_rate
qty_unchanged = prev_qty == new_qty qty_unchanged = prev_qty == new_qty
uom_unchanged = prev_uom == new_uom
conversion_factor_unchanged = prev_con_fac == new_con_fac conversion_factor_unchanged = prev_con_fac == new_con_fac
date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc
if rate_unchanged and qty_unchanged and conversion_factor_unchanged and date_unchanged: if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged:
continue continue
validate_quantity(child_item, d) validate_quantity(child_item, d)
@ -1291,6 +1333,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
else: else:
child_item.conversion_factor = flt(d.get('conversion_factor')) child_item.conversion_factor = flt(d.get('conversion_factor'))
if d.get("uom"):
child_item.uom = d.get("uom")
conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(d.get('conversion_factor')) or conversion_factor
if d.get("delivery_date") and parent_doctype == 'Sales Order': if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date') child_item.delivery_date = d.get('delivery_date')
@ -1356,12 +1403,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_receiving_percentage() parent.update_receiving_percentage()
if parent.is_subcontracted == "Yes": if parent.is_subcontracted == "Yes":
parent.update_reserved_qty_for_subcontract() parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied("supplied_items")
parent.save()
else: else:
parent.update_reserved_qty() parent.update_reserved_qty()
parent.update_project() parent.update_project()
parent.update_prevdoc_status('submit') parent.update_prevdoc_status('submit')
parent.update_delivery_status() parent.update_delivery_status()
parent.reload()
validate_workflow_conditions(parent)
parent.update_blanket_order() parent.update_blanket_order()
parent.update_billing_percentage() parent.update_billing_percentage()
parent.set_status() parent.set_status()

View File

@ -165,9 +165,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
AND company = %(company)s AND company = %(company)s
AND account_currency = %(currency)s AND account_currency = %(currency)s
AND `{searchfield}` LIKE %(txt)s AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name ORDER BY idx DESC, name
LIMIT %(offset)s, %(limit)s LIMIT %(offset)s, %(limit)s
""".format(account_type_condition=account_type_condition, searchfield=searchfield), """.format(
account_type_condition=account_type_condition,
searchfield=searchfield,
mcond=get_match_cond(doctype)
),
dict( dict(
account_types=filters.get("account_type"), account_types=filters.get("account_type"),
company=filters.get("company"), company=filters.get("company"),
@ -359,9 +364,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
if filters.get("is_return"): if filters.get("is_return"):
having_clause = "" having_clause = ""
meta = frappe.get_meta("Batch", cached=True)
searchfields = meta.get_search_fields()
search_columns = ''
if searchfields:
search_columns = ", " + ", ".join(searchfields)
if args.get('warehouse'): if args.get('warehouse'):
searchfields = ['batch.' + field for field in searchfields]
if searchfields:
search_columns = ", " + ", ".join(searchfields)
batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date) concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
{search_columns}
from `tabStock Ledger Entry` sle from `tabStock Ledger Entry` sle
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where where
@ -377,6 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
group by batch_no {having_clause} group by batch_no {having_clause}
order by batch.expiry_date, sle.batch_no desc order by batch.expiry_date, sle.batch_no desc
limit %(start)s, %(page_len)s""".format( limit %(start)s, %(page_len)s""".format(
search_columns = search_columns,
cond=cond, cond=cond,
match_conditions=get_match_cond(doctype), match_conditions=get_match_cond(doctype),
having_clause = having_clause having_clause = having_clause
@ -384,7 +402,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
return batch_nos return batch_nos
else: else:
return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) from `tabBatch` batch return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date)
{search_columns}
from `tabBatch` batch
where batch.disabled = 0 where batch.disabled = 0
and item = %(item_code)s and item = %(item_code)s
and (name like %(txt)s and (name like %(txt)s
@ -394,7 +414,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{0} {0}
{match_conditions} {match_conditions}
order by expiry_date, name desc order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, match_conditions=get_match_cond(doctype)), args)
@frappe.whitelist() @frappe.whitelist()

View File

@ -242,7 +242,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
'type': data.type, 'type': data.type,
'amount': -1 * paid_amount, 'amount': -1 * paid_amount,
'base_amount': -1 * base_paid_amount, 'base_amount': -1 * base_paid_amount,
'account': data.account 'account': data.account,
'default': data.default
}) })
if doc.is_pos: if doc.is_pos:
doc.paid_amount = -1 * source.paid_amount doc.paid_amount = -1 * source.paid_amount

View File

@ -81,6 +81,7 @@ class SellingController(StockController):
party_details = _get_party_details(customer, party_details = _get_party_details(customer,
ignore_permissions=self.flags.ignore_permissions, ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype, company=self.company, doctype=self.doctype, company=self.company,
posting_date=self.get('posting_date'),
fetch_payment_terms_template=fetch_payment_terms_template, fetch_payment_terms_template=fetch_payment_terms_template,
party_address=self.customer_address, shipping_address=self.shipping_address_name) party_address=self.customer_address, shipping_address=self.shipping_address_name)
if not self.meta.get_field("sales_team"): if not self.meta.get_field("sales_team"):

View File

@ -255,7 +255,7 @@ class StatusUpdater(Document):
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s` from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s" where `%(second_join_field)s`="%(detail_id)s"
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args
if args['detail_id']: if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = "" if not args.get("extra_cond"): args["extra_cond"] = ""

View File

@ -8,7 +8,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Reports", "label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Opportunity\",\n \"name\": \"Minutes to First Response for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -42,7 +42,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "CRM", "label": "CRM",
"modified": "2020-05-28 13:33:52.906750", "modified": "2020-08-11 18:55:18.238900",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "CRM", "name": "CRM",

View File

@ -27,9 +27,6 @@ class Lead(SellingController):
def after_insert(self): def after_insert(self):
self.update_links() self.update_links()
# after the address and contact are created, flush the field values
# to avoid inconsistent reporting in case the documents are changed
self.flush_address_and_contact_fields()
def validate(self): def validate(self):
self.set_lead_name() self.set_lead_name()
@ -210,14 +207,6 @@ class Lead(SellingController):
}) })
self.contact_doc.save() self.contact_doc.save()
def flush_address_and_contact_fields(self):
fields = ['address_type', 'address_line1', 'address_line2', 'address_title',
'city', 'county', 'country', 'fax', 'pincode', 'state']
for field in fields:
self.set(field, None)
@frappe.whitelist() @frappe.whitelist()
def make_customer(source_name, target_doc=None): def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc) return _make_customer(source_name, target_doc)
@ -376,3 +365,8 @@ def get_lead_with_phone_number(number):
lead = leads[0].name if leads else None lead = leads[0].name if leads else None
return lead return lead
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")

View File

@ -24,7 +24,7 @@
"converted_by", "converted_by",
"sales_stage", "sales_stage",
"order_lost_reason", "order_lost_reason",
"mins_to_first_response", "first_response_time",
"expected_closing", "expected_closing",
"next_contact", "next_contact",
"contact_by", "contact_by",
@ -152,13 +152,6 @@
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{
"bold": 1,
"fieldname": "mins_to_first_response",
"fieldtype": "Float",
"label": "Mins to first response",
"read_only": 1
},
{ {
"fieldname": "expected_closing", "fieldname": "expected_closing",
"fieldtype": "Date", "fieldtype": "Date",
@ -419,12 +412,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Converted By", "label": "Converted By",
"options": "User" "options": "User"
},
{
"bold": 1,
"fieldname": "first_response_time",
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
} }
], ],
"icon": "fa fa-info-sign", "icon": "fa fa-info-sign",
"idx": 195, "idx": 195,
"links": [], "links": [],
"modified": "2020-08-11 17:34:35.066961", "modified": "2020-08-12 17:34:35.066961",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity", "name": "Opportunity",

View File

@ -267,6 +267,9 @@ def make_quotation(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def make_request_for_quotation(source_name, target_doc=None): def make_request_for_quotation(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.conversion_factor = 1.0
doclist = get_mapped_doc("Opportunity", source_name, { doclist = get_mapped_doc("Opportunity", source_name, {
"Opportunity": { "Opportunity": {
"doctype": "Request for Quotation" "doctype": "Request for Quotation"
@ -277,7 +280,8 @@ def make_request_for_quotation(source_name, target_doc=None):
["name", "opportunity_item"], ["name", "opportunity_item"],
["parent", "opportunity"], ["parent", "opportunity"],
["uom", "uom"] ["uom", "uom"]
] ],
"postprocess": update_item
} }
}, target_doc) }, target_doc)

View File

@ -82,7 +82,8 @@ def make_opportunity(**args):
if args.with_items: if args.with_items:
opp_doc.append('items', { opp_doc.append('items', {
"item_code": args.item_code or "_Test Item", "item_code": args.item_code or "_Test Item",
"qty": args.qty or 1 "qty": args.qty or 1,
"uom": "_Test UOM"
}) })
opp_doc.insert() opp_doc.insert()

View File

@ -1,33 +1,44 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Minutes to First Response for Opportunity"] = { frappe.query_reports["First Response Time for Opportunity"] = {
"filters": [ "filters": [
{ {
"fieldname": "from_date", "fieldname": "from_date",
"label": __("From Date"), "label": __("From Date"),
"fieldtype": "Date", "fieldtype": "Date",
'reqd': 1, "reqd": 1,
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
}, },
{ {
"fieldname": "to_date", "fieldname": "to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
'reqd': 1, "reqd": 1,
"default": frappe.datetime.nowdate() "default": frappe.datetime.nowdate()
}, },
], ],
get_chart_data: function (columns, result) { get_chart_data: function (_columns, result) {
return { return {
data: { data: {
labels: result.map(d => d[0]), labels: result.map(d => d[0]),
datasets: [{ datasets: [{
name: 'Mins to first response', name: "First Response Time",
values: result.map(d => d[1]) values: result.map(d => d[1])
}] }]
}, },
type: 'line', type: "line",
tooltipOptions: {
formatTooltipY: d => {
let duration_options = {
hide_days: 0,
hide_seconds: 0
};
value = frappe.utils.get_formatted_duration(d, duration_options);
return value;
} }
} }
} }
}
};

View File

@ -0,0 +1,28 @@
{
"add_total_row": 0,
"creation": "2020-08-10 18:34:19.083872",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "Test 2",
"modified": "2020-08-10 18:34:19.083872",
"modified_by": "Administrator",
"module": "CRM",
"name": "First Response Time for Opportunity",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Opportunity",
"report_name": "First Response Time for Opportunity",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -0,0 +1,35 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date',
'width': 300
},
{
'fieldname': 'first_response_time',
'fieldtype': 'Duration',
'label': 'First Response Time',
'width': 300
},
]
data = frappe.db.sql('''
SELECT
date(creation) as creation_date,
avg(first_response_time) as avg_response_time
FROM tabOpportunity
WHERE
date(creation) between %s and %s
and first_response_time > 0
GROUP BY creation_date
ORDER BY creation_date desc
''', (filters.from_date, filters.to_date))
return columns, data

View File

@ -1,26 +0,0 @@
{
"add_total_row": 0,
"apply_user_permissions": 0,
"creation": "2016-06-17 11:28:25.867258",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:06:08.801109",
"modified_by": "Administrator",
"module": "CRM",
"name": "Minutes to First Response for Opportunity",
"owner": "Administrator",
"ref_doctype": "Opportunity",
"report_name": "Minutes to First Response for Opportunity",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -1,28 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date'
},
{
'fieldname': 'mins',
'fieldtype': 'Float',
'label': 'Mins to First Response'
},
]
data = frappe.db.sql('''select date(creation) as creation_date,
avg(mins_to_first_response) as mins
from tabOpportunity
where date(creation) between %s and %s
and mins_to_first_response > 0
group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
return columns, data

View File

@ -13,7 +13,7 @@
"fields": [ "fields": [
{ {
"fieldname": "question", "fieldname": "question",
"fieldtype": "Small Text", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
"label": "Question", "label": "Question",
"reqd": 1 "reqd": 1
@ -34,7 +34,7 @@
"read_only": 1 "read_only": 1
} }
], ],
"modified": "2019-05-30 18:39:21.880974", "modified": "2020-09-24 18:39:21.880974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Question", "name": "Question",

View File

@ -20,14 +20,14 @@
{ {
"fetch_from": "question_link.question", "fetch_from": "question_link.question",
"fieldname": "question", "fieldname": "question",
"fieldtype": "Data", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
"label": "Question", "label": "Question",
"read_only": 1 "read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-06-12 12:24:02.312577", "modified": "2020-09-24 12:24:02.312577",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Quiz Question", "name": "Quiz Question",

View File

@ -3,15 +3,15 @@
frappe.ui.form.on('Student', { frappe.ui.form.on('Student', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("guardian", "guardian_name", "guardian_name"); frm.add_fetch('guardian', 'guardian_name', 'guardian_name');
frm.add_fetch("student", "title", "full_name"); frm.add_fetch('student', 'title', 'full_name');
frm.add_fetch("student", "gender", "gender"); frm.add_fetch('student', 'gender', 'gender');
frm.add_fetch("student", "date_of_birth", "date_of_birth"); frm.add_fetch('student', 'date_of_birth', 'date_of_birth');
frm.set_query("student", "siblings", function(doc, cdt, cdn) { frm.set_query('student', 'siblings', function(doc) {
return { return {
"filters": { 'filters': {
"name": ["!=", doc.name] 'name': ['!=', doc.name]
} }
}; };
}) })
@ -25,6 +25,12 @@ frappe.ui.form.on('Student', {
{party_type:'Student', party:frm.doc.name}); {party_type:'Student', party:frm.doc.name});
}); });
} }
frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => {
if (cint(r.user_creation_skip) !== 1) {
frm.set_df_property('student_email_id', 'reqd', 1);
}
});
} }
}); });

View File

@ -102,7 +102,6 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Student Email Address", "label": "Student Email Address",
"reqd": 1,
"unique": 1 "unique": 1
}, },
{ {
@ -255,7 +254,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-07-23 18:14:06.366442", "modified": "2020-09-07 19:28:08.914568",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Student", "name": "Student",

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Student Applicant", {
}, },
refresh: function(frm) { refresh: function(frm) {
if(frm.doc.application_status== "Applied" && frm.doc.docstatus== 1 ) { if (frm.doc.application_status==="Applied" && frm.doc.docstatus===1 ) {
frm.add_custom_button(__("Approve"), function() { frm.add_custom_button(__("Approve"), function() {
frm.set_value("application_status", "Approved"); frm.set_value("application_status", "Approved");
frm.save_or_update(); frm.save_or_update();
@ -20,10 +20,11 @@ frappe.ui.form.on("Student Applicant", {
}, 'Actions'); }, 'Actions');
} }
if(frm.doc.application_status== "Approved" && frm.doc.docstatus== 1 ) { if (frm.doc.application_status === "Approved" && frm.doc.docstatus === 1) {
frm.add_custom_button(__("Enroll"), function() { frm.add_custom_button(__("Enroll"), function() {
frm.events.enroll(frm) frm.events.enroll(frm)
}).addClass("btn-primary"); }).addClass("btn-primary");
frm.add_custom_button(__("Reject"), function() { frm.add_custom_button(__("Reject"), function() {
frm.set_value("application_status", "Rejected"); frm.set_value("application_status", "Rejected");
frm.save_or_update(); frm.save_or_update();
@ -35,7 +36,13 @@ frappe.ui.form.on("Student Applicant", {
frappe.hide_msgprint(true); frappe.hide_msgprint(true);
frappe.show_progress(__("Enrolling student"), data.progress[0],data.progress[1]); frappe.show_progress(__("Enrolling student"), data.progress[0],data.progress[1]);
} }
}) });
frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => {
if (cint(r.user_creation_skip) !== 1) {
frm.set_df_property("student_email_id", "reqd", 1);
}
});
}, },
enroll: function(frm) { enroll: function(frm) {

View File

@ -0,0 +1,40 @@
{
"cards": [
{
"hidden": 0,
"label": "Marketplace",
"links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Payments",
"links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Settings",
"links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]"
}
],
"category": "Modules",
"charts": [],
"creation": "2020-08-20 19:30:48.138801",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"extends": "Integrations",
"extends_another_page": 1,
"hide_custom": 1,
"idx": 0,
"is_standard": 1,
"label": "ERPNext Integrations",
"modified": "2020-08-23 16:30:51.494655",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": []
}

View File

@ -2,81 +2,90 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals import plaid
from frappe import _ import requests
from frappe.utils.password import get_decrypted_password from plaid.errors import APIError, ItemError, InvalidRequestError
from plaid import Client
from plaid.errors import APIError, ItemError
import frappe import frappe
import requests from frappe import _
class PlaidConnector(): class PlaidConnector():
def __init__(self, access_token=None): def __init__(self, access_token=None):
plaid_settings = frappe.get_single("Plaid Settings")
self.config = {
"plaid_client_id": plaid_settings.plaid_client_id,
"plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env
}
self.client = Client(client_id=self.config.get("plaid_client_id"),
secret=self.config.get("plaid_secret"),
public_key=self.config.get("plaid_public_key"),
environment=self.config.get("plaid_env")
)
self.access_token = access_token self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings")
self.products = ["auth", "transactions"]
self.client_name = frappe.local.site
self.client = plaid.Client(
client_id=self.settings.plaid_client_id,
secret=self.settings.get_password("plaid_secret"),
environment=self.settings.plaid_env,
api_version="2019-05-29"
)
def get_access_token(self, public_token): def get_access_token(self, public_token):
if public_token is None: if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token) response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token'] access_token = response["access_token"]
return access_token return access_token
def get_link_token(self):
token_request = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}
}
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
frappe.msgprint(_("Please check your Plaid client ID and secret values"))
except APIError as e:
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.throw(_(str(e)), title=_("Authentication Failed"))
else:
return response["link_token"]
def auth(self): def auth(self):
try: try:
self.client.Auth.get(self.access_token) self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e: except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED': if e.code == "ITEM_LOGIN_REQUIRED":
pass
else:
pass pass
except APIError as e: except APIError as e:
if e.code == 'PLANNED_MAINTENANCE': if e.code == "PLANNED_MAINTENANCE":
pass
else:
pass pass
except requests.Timeout: except requests.Timeout:
pass pass
except Exception as e: except Exception as e:
print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None): def get_transactions(self, start_date, end_date, account_id=None):
try:
self.auth() self.auth()
kwargs = dict(
access_token=self.access_token,
start_date=start_date,
end_date=end_date
)
if account_id: if account_id:
account_ids = [account_id] kwargs.update(dict(account_ids=[account_id]))
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) try:
response = self.client.Transactions.get(**kwargs)
else: transactions = response["transactions"]
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date) while len(transactions) < response["total_transactions"]:
transactions = response['transactions']
while len(transactions) < response['total_transactions']:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response['transactions']) transactions.extend(response["transactions"])
return transactions return transactions
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@ -7,9 +7,9 @@ frappe.ui.form.on('Plaid Settings', {
enabled: function (frm) { enabled: function (frm) {
frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
frm.toggle_reqd('plaid_secret', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled);
frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
frm.toggle_reqd('plaid_env', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled);
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.enabled) { if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => { frm.add_custom_button('Link a new bank account', () => {
@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
erpnext.integrations.plaidLink = class plaidLink { erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) { constructor(parent) {
this.frm = parent; this.frm = parent;
this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config(); this.init_config();
} }
init_config() { async init_config() {
const me = this; this.product = ["auth", "transactions"];
me.plaid_env = me.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
me.plaid_public_key = me.frm.doc.plaid_public_key; this.client_name = frappe.boot.sitename;
me.client_name = frappe.boot.sitename; this.token = await this.frm.call("get_link_token").then(resp => resp.message);
me.init_plaid(); this.init_plaid();
} }
init_plaid() { init_plaid() {
@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = window.Plaid.create({ me.linkHandler = Plaid.create({
clientName: me.client_name, clientName: me.client_name,
product: me.product,
env: me.plaid_env, env: me.plaid_env,
key: me.plaid_public_key, token: me.token,
onSuccess: me.plaid_success, onSuccess: me.plaid_success
product: me.product
}); });
} }
onScriptError(error) { onScriptError(error) {
frappe.msgprint('There was an issue loading the link-initialize.js script'); frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error); frappe.msgprint(error);
} }
@ -94,12 +93,16 @@ erpnext.integrations.plaidLink = class plaidLink {
reqd: 1 reqd: 1
}, (data) => { }, (data) => {
me.company = data.company; me.company = data.company;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
.then((result) => { token: token,
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, response: response
bank: result, company: me.company}); }).then((result) => {
}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
.then(() => { response: response,
bank: result,
company: me.company
});
}).then(() => {
frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' }); frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
}); });
}, __("Select a company"), __("Continue")); }, __("Select a company"), __("Continue"));

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2018-10-25 10:02:48.656165", "creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -12,7 +11,6 @@
"plaid_client_id", "plaid_client_id",
"plaid_secret", "plaid_secret",
"column_break_7", "column_break_7",
"plaid_public_key",
"plaid_env" "plaid_env"
], ],
"fields": [ "fields": [
@ -41,12 +39,6 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Plaid Secret" "label": "Plaid Secret"
}, },
{
"fieldname": "plaid_public_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Plaid Public Key"
},
{ {
"fieldname": "plaid_env", "fieldname": "plaid_env",
"fieldtype": "Select", "fieldtype": "Select",
@ -69,8 +61,7 @@
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "modified": "2020-09-12 02:31:44.542385",
"modified": "2020-02-07 15:21:11.616231",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Plaid Settings", "name": "Plaid Settings",

View File

@ -2,37 +2,44 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json import json
from frappe import _
from frappe.model.document import Document import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months from frappe import _
from frappe.desk.doctype.tag.tag import add_tag from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document): class PlaidSettings(Document):
pass @staticmethod
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@frappe.whitelist() @frappe.whitelist()
def plaid_configuration(): def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"): if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings") plaid_settings = frappe.get_single("Plaid Settings")
return { return {
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env, "plaid_env": plaid_settings.plaid_env,
"link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site "client_name": frappe.local.site
} }
else:
return "disabled" return "disabled"
@frappe.whitelist() @frappe.whitelist()
def add_institution(token, response): def add_institution(token, response):
response = json.loads(response) response = json.loads(response)
plaid = PlaidConnector() plaid = PlaidConnector()
access_token = plaid.get_access_token(token) access_token = plaid.get_access_token(token)
bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]): if not frappe.db.exists("Bank", response["institution"]["name"]):
try: try:
@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert() bank.insert()
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
else: else:
bank = frappe.get_doc("Bank", response["institution"]["name"]) bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token bank.plaid_access_token = access_token
@ -52,6 +58,7 @@ def add_institution(token, response):
return bank return bank
@frappe.whitelist() @frappe.whitelist()
def add_bank_accounts(response, bank, company): def add_bank_accounts(response, bank, company):
try: try:
@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert() new_account.insert()
result.append(new_account.name) result.append(new_account.name)
except frappe.UniqueValidationError: except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result return result
def add_account_type(account_type): def add_account_type(account_type):
try: try:
frappe.get_doc({ frappe.get_doc({
@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@frappe.whitelist() @frappe.whitelist()
def sync_transactions(bank, bank_account): def sync_transactions(bank, bank_account):
'''Sync transactions based on the last integration date as the start date, after the sync is completed """Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date''' add the transaction date of the oldest transaction as the last integration date."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date: if last_transaction_date:
start_date = formatdate(last_transaction_date, "YYYY-MM-dd") start_date = formatdate(last_transaction_date, "YYYY-MM-dd")
@ -147,10 +155,10 @@ def sync_transactions(bank, bank_account):
len(result), bank_account, start_date, end_date)) len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None): def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None access_token = None
@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions return transactions
def new_bank_transaction(transaction): def new_bank_transaction(transaction):
result = [] result = []
@ -182,8 +191,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled" status = "Pending" if transaction["pending"] == "True" else "Settled"
try:
tags = [] tags = []
try:
tags += transaction["category"] tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])] tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError: except KeyError:
@ -216,6 +225,7 @@ def new_bank_transaction(transaction):
return result return result
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@ -223,4 +233,8 @@ def automatic_synchronization():
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts: for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)

View File

@ -1,14 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json import json
from frappe.utils.response import json_handler import unittest
import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype, add_account_type, add_bank_accounts,
new_bank_transaction, get_plaid_configuration)
from frappe.utils.response import json_handler
class TestPlaidSettings(unittest.TestCase): class TestPlaidSettings(unittest.TestCase):
def setUp(self): def setUp(self):
@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self): def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0) frappe.db.set_value("Plaid Settings", None, "enabled", 0)
self.assertTrue(plaid_configuration() == "disabled") self.assertTrue(get_plaid_configuration() == "disabled")
def test_add_account_type(self): def test_add_account_type(self):
add_account_type("brokerage") add_account_type("brokerage")

View File

@ -258,8 +258,8 @@
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2020-05-28 12:32:11.384757", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "umair@erpnext.com", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Shopify Settings", "name": "Shopify Settings",
"owner": "Administrator", "owner": "Administrator",

View File

@ -117,12 +117,12 @@
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-05-08 13:45:57.226530", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Schedule Time Slot", "name": "Healthcare Schedule Time Slot",
"name_case": "", "name_case": "",
"owner": "rmehta@gmail.com", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "read_only": 0,

View File

@ -34,10 +34,10 @@ frappe.ui.form.on('Lab Test', {
if (frm.doc.docstatus === 1 && frm.doc.status !== 'Approved' && frm.doc.status !== 'Rejected') { if (frm.doc.docstatus === 1 && frm.doc.status !== 'Approved' && frm.doc.status !== 'Rejected') {
frm.add_custom_button(__('Approve'), function () { frm.add_custom_button(__('Approve'), function () {
status_update(1, frm); status_update(1, frm);
}); }, __('Actions'));
frm.add_custom_button(__('Reject'), function () { frm.add_custom_button(__('Reject'), function () {
status_update(0, frm); status_update(0, frm);
}); }, __('Actions'));
} }
} }
@ -179,23 +179,6 @@ var show_lab_tests = function (frm, lab_test_list) {
d.show(); d.show();
}; };
cur_frm.cscript.custom_before_submit = function (doc) {
if (doc.normal_test_items) {
for (let result in doc.normal_test_items) {
if (!doc.normal_test_items[result].result_value && !doc.normal_test_items[result].allow_blank && doc.normal_test_items[result].require_result_value) {
frappe.throw(__('Please input all required result values'));
}
}
}
if (doc.descriptive_test_items) {
for (let result in doc.descriptive_test_items) {
if (!doc.descriptive_test_items[result].result_value && !doc.descriptive_test_items[result].allow_blank && doc.descriptive_test_items[result].require_result_value) {
frappe.throw(__('Please input all required result values'));
}
}
}
};
var make_dialog = function (frm, emailed, printed) { var make_dialog = function (frm, emailed, printed) {
var number = frm.doc.mobile; var number = frm.doc.mobile;
@ -203,7 +186,7 @@ var make_dialog = function (frm, emailed, printed) {
title: 'Send SMS', title: 'Send SMS',
width: 400, width: 400,
fields: [ fields: [
{ fieldname: 'sms_type', fieldtype: 'Select', label: 'Type', options: ['Emailed', 'Printed'] }, { fieldname: 'result_format', fieldtype: 'Select', label: 'Result Format', options: ['Emailed', 'Printed'] },
{ fieldname: 'number', fieldtype: 'Data', label: 'Mobile Number', reqd: 1 }, { fieldname: 'number', fieldtype: 'Data', label: 'Mobile Number', reqd: 1 },
{ fieldname: 'message', fieldtype: 'Small Text', label: 'Message', reqd: 1 } { fieldname: 'message', fieldtype: 'Small Text', label: 'Message', reqd: 1 }
], ],
@ -217,22 +200,22 @@ var make_dialog = function (frm, emailed, printed) {
dialog.hide(); dialog.hide();
} }
}); });
if (frm.doc.report_preference == 'Print') { if (frm.doc.report_preference === 'Print') {
dialog.set_values({ dialog.set_values({
'sms_type': 'Printed', 'result_format': 'Printed',
'number': number, 'number': number,
'message': printed 'message': printed
}); });
} else { } else {
dialog.set_values({ dialog.set_values({
'sms_type': 'Emailed', 'result_format': 'Emailed',
'number': number, 'number': number,
'message': emailed 'message': emailed
}); });
} }
var fd = dialog.fields_dict; var fd = dialog.fields_dict;
$(fd.sms_type.input).change(function () { $(fd.result_format.input).change(function () {
if (dialog.get_value('sms_type') == 'Emailed') { if (dialog.get_value('result_format') === 'Emailed') {
dialog.set_values({ dialog.set_values({
'number': number, 'number': number,
'message': emailed 'message': emailed

View File

@ -84,7 +84,7 @@
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"options": "LP-", "options": "HLC-LAB-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"report_hide": 1, "report_hide": 1,
"reqd": 1 "reqd": 1
@ -197,11 +197,10 @@
{ {
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"label": "Status", "label": "Status",
"options": "Draft\nCompleted\nApproved\nRejected\nCancelled", "options": "Draft\nCompleted\nApproved\nRejected\nCancelled",
"print_hide": 1,
"read_only": 1, "read_only": 1,
"report_hide": 1,
"search_index": 1 "search_index": 1
}, },
{ {
@ -249,8 +248,8 @@
{ {
"fieldname": "result_date", "fieldname": "result_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 1,
"label": "Result Date", "label": "Result Date",
"read_only": 1,
"search_index": 1 "search_index": 1
}, },
{ {
@ -354,7 +353,8 @@
}, },
{ {
"fieldname": "sb_normal", "fieldname": "sb_normal",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Compound Test Result"
}, },
{ {
"fieldname": "normal_test_items", "fieldname": "normal_test_items",
@ -369,11 +369,13 @@
{ {
"depends_on": "descriptive_toggle", "depends_on": "descriptive_toggle",
"fieldname": "organisms_section", "fieldname": "organisms_section",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Organism Test Result"
}, },
{ {
"fieldname": "sb_sensitivity", "fieldname": "sb_sensitivity",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Sensitivity Test Result"
}, },
{ {
"fieldname": "sensitivity_test_items", "fieldname": "sensitivity_test_items",
@ -383,8 +385,10 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"collapsible": 1,
"fieldname": "sb_comments", "fieldname": "sb_comments",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Comments"
}, },
{ {
"fieldname": "lab_test_comment", "fieldname": "lab_test_comment",
@ -531,7 +535,8 @@
}, },
{ {
"fieldname": "sb_descriptive", "fieldname": "sb_descriptive",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Descriptive Test Result"
}, },
{ {
"default": "0", "default": "0",
@ -550,7 +555,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-16 13:35:24.811062", "modified": "2020-07-30 18:18:38.516215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Lab Test", "name": "Lab Test",

View File

@ -6,23 +6,24 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, cstr from frappe.utils import getdate, cstr, get_link_to_form
class LabTest(Document): class LabTest(Document):
def validate(self):
if not self.is_new():
self.set_secondary_uom_result()
def on_submit(self): def on_submit(self):
self.validate_result_values()
self.db_set('submitted_date', getdate()) self.db_set('submitted_date', getdate())
self.db_set('status', 'Completed') self.db_set('status', 'Completed')
insert_lab_test_to_medical_record(self) insert_lab_test_to_medical_record(self)
def on_cancel(self): def on_cancel(self):
delete_lab_test_from_medical_record(self)
self.db_set('status', 'Cancelled') self.db_set('status', 'Cancelled')
delete_lab_test_from_medical_record(self)
self.reload() self.reload()
def validate(self):
if not self.is_new():
self.set_secondary_uom_result()
def on_update(self): def on_update(self):
if self.sensitivity_test_items: if self.sensitivity_test_items:
sensitivity = sorted(self.sensitivity_test_items, key=lambda x: x.antibiotic_sensitivity) sensitivity = sorted(self.sensitivity_test_items, key=lambda x: x.antibiotic_sensitivity)
@ -51,7 +52,20 @@ class LabTest(Document):
item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor)
except: except:
item.secondary_uom_result = '' item.secondary_uom_result = ''
frappe.msgprint(_('Result for Secondary UOM not calculated for row #{0}'.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:
for item in self.normal_test_items:
if not item.result_value and not item.allow_blank and item.require_result_value:
frappe.throw(_('Row #{0}: Please enter the result value for {1}').format(
item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results'))
if self.descriptive_test_items:
for item in self.descriptive_test_items:
if not item.result_value and not item.allow_blank and item.require_result_value:
frappe.throw(_('Row #{0}: Please enter the result value for {1}').format(
item.idx, frappe.bold(item.lab_test_particulars)), title=_('Mandatory Results'))
def create_test_from_template(lab_test): def create_test_from_template(lab_test):
@ -89,7 +103,7 @@ def create_multiple(doctype, docname):
lab_test_created = create_lab_test_from_encounter(docname) lab_test_created = create_lab_test_from_encounter(docname)
if lab_test_created: if lab_test_created:
frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created))) frappe.msgprint(_('Lab Test(s) {0} created successfully').format(lab_test_created), indicator='green')
else: else:
frappe.msgprint(_('No Lab Tests created')) frappe.msgprint(_('No Lab Tests created'))
@ -211,8 +225,9 @@ def create_sample_doc(template, patient, invoice, company = None):
'docstatus': 0, 'docstatus': 0,
'sample': template.sample 'sample': template.sample
}) })
if sample_exists: if sample_exists:
# Update Sample Collection by adding quantity # update sample collection by adding quantity
sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0])
quantity = int(sample_collection.sample_qty) + int(template.sample_qty) quantity = int(sample_collection.sample_qty) + int(template.sample_qty)
if template.sample_details: if template.sample_details:
@ -238,7 +253,7 @@ def create_sample_doc(template, patient, invoice, company = None):
sample_collection.company = company sample_collection.company = company
if template.sample_details: if template.sample_details:
sample_collection.sample_details = 'Test :' + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details sample_collection.sample_details = _('Test :') + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details
sample_collection.save(ignore_permissions=True) sample_collection.save(ignore_permissions=True)
return sample_collection return sample_collection
@ -248,26 +263,31 @@ def create_sample_collection(lab_test, template, patient, invoice):
sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) sample_collection = create_sample_doc(template, patient, invoice, lab_test.company)
if sample_collection: if sample_collection:
lab_test.sample = sample_collection.name lab_test.sample = sample_collection.name
sample_collection_doc = get_link_to_form('Sample Collection', sample_collection.name)
frappe.msgprint(_('Sample Collection {0} has been created').format(sample_collection_doc),
title=_('Sample Collection'), indicator='green')
return lab_test return lab_test
def load_result_format(lab_test, template, prescription, invoice): def load_result_format(lab_test, template, prescription, invoice):
if template.lab_test_template_type == 'Single': if template.lab_test_template_type == 'Single':
create_normals(template, lab_test) create_normals(template, lab_test)
elif template.lab_test_template_type == 'Compound': elif template.lab_test_template_type == 'Compound':
create_compounds(template, lab_test, False) create_compounds(template, lab_test, False)
elif template.lab_test_template_type == 'Descriptive': elif template.lab_test_template_type == 'Descriptive':
create_descriptives(template, lab_test) create_descriptives(template, lab_test)
elif template.lab_test_template_type == 'Grouped': elif template.lab_test_template_type == 'Grouped':
# Iterate for each template in the group and create one result for all. # Iterate for each template in the group and create one result for all.
for lab_test_group in template.lab_test_groups: for lab_test_group in template.lab_test_groups:
# Template_in_group = None # Template_in_group = None
if lab_test_group.lab_test_template: if lab_test_group.lab_test_template:
template_in_group = frappe.get_doc('Lab Test Template', template_in_group = frappe.get_doc('Lab Test Template', lab_test_group.lab_test_template)
lab_test_group.lab_test_template)
if template_in_group: if template_in_group:
if template_in_group.lab_test_template_type == 'Single': if template_in_group.lab_test_template_type == 'Single':
create_normals(template_in_group, lab_test) create_normals(template_in_group, lab_test)
elif template_in_group.lab_test_template_type == 'Compound': elif template_in_group.lab_test_template_type == 'Compound':
normal_heading = lab_test.append('normal_test_items') normal_heading = lab_test.append('normal_test_items')
normal_heading.lab_test_name = template_in_group.lab_test_name normal_heading.lab_test_name = template_in_group.lab_test_name
@ -275,6 +295,7 @@ def load_result_format(lab_test, template, prescription, invoice):
normal_heading.allow_blank = 1 normal_heading.allow_blank = 1
normal_heading.template = template_in_group.name normal_heading.template = template_in_group.name
create_compounds(template_in_group, lab_test, True) create_compounds(template_in_group, lab_test, True)
elif template_in_group.lab_test_template_type == 'Descriptive': elif template_in_group.lab_test_template_type == 'Descriptive':
descriptive_heading = lab_test.append('descriptive_test_items') descriptive_heading = lab_test.append('descriptive_test_items')
descriptive_heading.lab_test_name = template_in_group.lab_test_name descriptive_heading.lab_test_name = template_in_group.lab_test_name
@ -282,6 +303,7 @@ def load_result_format(lab_test, template, prescription, invoice):
descriptive_heading.allow_blank = 1 descriptive_heading.allow_blank = 1
descriptive_heading.template = template_in_group.name descriptive_heading.template = template_in_group.name
create_descriptives(template_in_group, lab_test) create_descriptives(template_in_group, lab_test)
else: # Lab Test Group - Add New Line else: # Lab Test Group - Add New Line
normal = lab_test.append('normal_test_items') normal = lab_test.append('normal_test_items')
normal.lab_test_name = lab_test_group.group_event normal.lab_test_name = lab_test_group.group_event
@ -292,6 +314,7 @@ def load_result_format(lab_test, template, prescription, invoice):
normal.allow_blank = lab_test_group.allow_blank normal.allow_blank = lab_test_group.allow_blank
normal.require_result_value = 1 normal.require_result_value = 1
normal.template = template.name normal.template = template.name
if template.lab_test_template_type != 'No Result': if template.lab_test_template_type != 'No Result':
if prescription: if prescription:
lab_test.prescription = prescription lab_test.prescription = prescription
@ -302,9 +325,10 @@ def load_result_format(lab_test, template, prescription, invoice):
@frappe.whitelist() @frappe.whitelist()
def get_employee_by_user_id(user_id): def get_employee_by_user_id(user_id):
emp_id = frappe.db.get_value('Employee', { 'user_id': user_id }) emp_id = frappe.db.exists('Employee', { 'user_id': user_id })
employee = frappe.get_doc('Employee', emp_id) if emp_id:
return employee return frappe.get_doc('Employee', emp_id)
return None
def insert_lab_test_to_medical_record(doc): def insert_lab_test_to_medical_record(doc):
table_row = False table_row = False

View File

@ -3,13 +3,16 @@
*/ */
frappe.listview_settings['Lab Test'] = { frappe.listview_settings['Lab Test'] = {
add_fields: ['name', 'status', 'invoiced'], add_fields: ['name', 'status', 'invoiced'],
filters: [['docstatus', '=', '0']], filters: [['docstatus', '=', '1']],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == 'Approved') { if (doc.status === 'Approved') {
return [__('Approved'), 'green', 'status, =, Approved']; return [__('Approved'), 'green', 'status, =, Approved'];
} } else if (doc.status === 'Rejected') {
if (doc.status == 'Rejected') {
return [__('Rejected'), 'orange', 'status, =, Rejected']; return [__('Rejected'), 'orange', 'status, =, Rejected'];
} else if (doc.status === 'Completed') {
return [__('Completed'), 'green', 'status, =, Completed'];
} else if (doc.status === 'Cancelled') {
return [__('Cancelled'), 'red', 'status, =, Cancelled'];
} }
}, },
onload: function (listview) { onload: function (listview) {
@ -21,7 +24,7 @@ frappe.listview_settings['Lab Test'] = {
var create_multiple_dialog = function (listview) { var create_multiple_dialog = function (listview) {
var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({
title: 'Create Multiple Lab Test', title: 'Create Multiple Lab Tests',
width: 100, width: 100,
fields: [ fields: [
{ fieldtype: 'Link', label: 'Patient', fieldname: 'patient', options: 'Patient', reqd: 1 }, { fieldtype: 'Link', label: 'Patient', fieldname: 'patient', options: 'Patient', reqd: 1 },
@ -41,7 +44,7 @@ var create_multiple_dialog = function (listview) {
} }
} }
], ],
primary_action_label: __('Create Lab Test'), primary_action_label: __('Create'),
primary_action: function () { primary_action: function () {
frappe.call({ frappe.call({
method: 'erpnext.healthcare.doctype.lab_test.lab_test.create_multiple', method: 'erpnext.healthcare.doctype.lab_test.lab_test.create_multiple',

View File

@ -3,8 +3,204 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe
# test_records = frappe.get_test_records('Lab Test') from frappe.utils import getdate, nowtime
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
from erpnext.healthcare.doctype.patient_medical_record.test_patient_medical_record import create_lab_test_template as create_blood_test_template
class TestLabTest(unittest.TestCase): class TestLabTest(unittest.TestCase):
pass def test_lab_test_item(self):
lab_template = create_lab_test_template()
self.assertTrue(frappe.db.exists('Item', lab_template.item))
self.assertEqual(frappe.db.get_value('Item Price', {'item_code':lab_template.item}, 'price_list_rate'), lab_template.lab_test_rate)
lab_template.disabled = 1
lab_template.save()
self.assertEquals(frappe.db.get_value('Item', lab_template.item, 'disabled'), 1)
lab_template.reload()
lab_template.disabled = 0
lab_template.save()
def test_descriptive_lab_test(self):
lab_template = create_lab_test_template()
# blank result value not allowed as per template
lab_test = create_lab_test(lab_template)
lab_test.descriptive_test_items[0].result_value = 12
lab_test.descriptive_test_items[2].result_value = 1
lab_test.save()
self.assertRaises(frappe.ValidationError, lab_test.submit)
def test_sample_collection(self):
frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 1)
lab_template = create_lab_test_template()
lab_test = create_lab_test(lab_template)
lab_test.descriptive_test_items[0].result_value = 12
lab_test.descriptive_test_items[1].result_value = 1
lab_test.descriptive_test_items[2].result_value = 2.3
lab_test.save()
# check sample collection created
self.assertTrue(frappe.db.exists('Sample Collection', {'sample': lab_template.sample}))
frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 0)
lab_test = create_lab_test(lab_template)
lab_test.descriptive_test_items[0].result_value = 12
lab_test.descriptive_test_items[1].result_value = 1
lab_test.descriptive_test_items[2].result_value = 2.3
lab_test.save()
# sample collection should not be created
lab_test.reload()
self.assertEquals(lab_test.sample, None)
def test_create_lab_tests_from_sales_invoice(self):
sales_invoice = create_sales_invoice()
create_multiple('Sales Invoice', sales_invoice.name)
sales_invoice.reload()
self.assertIsNotNone(sales_invoice.items[0].reference_dn)
self.assertIsNotNone(sales_invoice.items[1].reference_dn)
def test_create_lab_tests_from_patient_encounter(self):
patient_encounter = create_patient_encounter()
create_multiple('Patient Encounter', patient_encounter.name)
patient_encounter.reload()
self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created)
self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created)
def create_lab_test_template(test_sensitivity=0, sample_collection=1):
medical_department = create_medical_department()
if frappe.db.exists('Lab Test Template', 'Insulin Resistance'):
return frappe.get_doc('Lab Test Template', 'Insulin Resistance')
template = frappe.new_doc('Lab Test Template')
template.lab_test_name = 'Insulin Resistance'
template.lab_test_template_type = 'Descriptive'
template.lab_test_code = 'Insulin Resistance'
template.lab_test_group = 'Services'
template.department = medical_department
template.is_billable = 1
template.lab_test_description = 'Insulin Resistance'
template.lab_test_rate = 2000
for entry in ['FBS', 'Insulin', 'IR']:
template.append('descriptive_test_templates', {
'particulars': entry,
'allow_blank': 1 if entry=='IR' else 0
})
if test_sensitivity:
template.sensitivity = 1
if sample_collection:
template.sample = create_lab_test_sample()
template.sample_qty = 5.0
template.save()
return template
def create_medical_department():
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department')
if not medical_department:
medical_department = frappe.new_doc('Medical Department')
medical_department.department = '_Test Medical Department'
medical_department.save()
medical_department = medical_department.name
return medical_department
def create_lab_test(lab_template):
patient = create_patient()
lab_test = frappe.new_doc('Lab Test')
lab_test.template = lab_template.name
lab_test.patient = patient
lab_test.patient_sex = 'Female'
lab_test.save()
return lab_test
def create_lab_test_sample():
blood_sample = frappe.db.exists('Lab Test Sample', 'Blood Sample')
if blood_sample:
return blood_sample
sample = frappe.new_doc('Lab Test Sample')
sample.sample = 'Blood Sample'
sample.sample_uom = 'U/ml'
sample.save()
return sample.name
def create_sales_invoice():
patient = create_patient()
medical_department = create_medical_department()
insulin_resistance_template = create_lab_test_template()
blood_test_template = create_blood_test_template(medical_department)
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.patient = patient
sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer')
sales_invoice.due_date = getdate()
sales_invoice.company = '_Test Company'
sales_invoice.debit_to = get_receivable_account('_Test Company')
tests = [insulin_resistance_template, blood_test_template]
for entry in tests:
sales_invoice.append('items', {
'item_code': entry.item,
'item_name': entry.lab_test_name,
'description': entry.lab_test_description,
'qty': 1,
'uom': 'Nos',
'conversion_factor': 1,
'income_account': get_income_account(None, '_Test Company'),
'rate': entry.lab_test_rate,
'amount': entry.lab_test_rate
})
sales_invoice.set_missing_values()
sales_invoice.submit()
return sales_invoice
def create_patient_encounter():
patient = create_patient()
medical_department = create_medical_department()
insulin_resistance_template = create_lab_test_template()
blood_test_template = create_blood_test_template(medical_department)
patient_encounter = frappe.new_doc('Patient Encounter')
patient_encounter.patient = patient
patient_encounter.practitioner = create_practitioner()
patient_encounter.encounter_date = getdate()
patient_encounter.encounter_time = nowtime()
tests = [insulin_resistance_template, blood_test_template]
for entry in tests:
patient_encounter.append('lab_test_prescription', {
'lab_test_code': entry.item,
'lab_test_name': entry.lab_test_name
})
patient_encounter.submit()
return patient_encounter
def create_practitioner():
practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner')
if not practitioner:
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = '_Test Healthcare Practitioner'
practitioner.gender = 'Female'
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
practitioner = practitioner.name
return practitioner

View File

@ -93,7 +93,8 @@
"depends_on": "secondary_uom", "depends_on": "secondary_uom",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Conversion Factor" "label": "Conversion Factor",
"mandatory_depends_on": "secondary_uom"
}, },
{ {
"default": "0", "default": "0",
@ -106,7 +107,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-06-24 10:59:01.921924", "modified": "2020-07-30 12:36:03.082391",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Lab Test Group Template", "name": "Lab Test Group Template",

View File

@ -34,14 +34,15 @@
"descriptive_test_templates", "descriptive_test_templates",
"section_break_group", "section_break_group",
"lab_test_groups", "lab_test_groups",
"medical_coding_section",
"medical_code_standard",
"medical_code",
"sb_sample_collection", "sb_sample_collection",
"sample", "sample",
"sample_uom", "sample_uom",
"sample_qty", "sample_qty",
"column_break_33",
"sample_details", "sample_details",
"medical_coding_section",
"medical_code",
"medical_code_standard",
"worksheet_section", "worksheet_section",
"worksheet_instructions", "worksheet_instructions",
"result_legend_section", "result_legend_section",
@ -112,7 +113,7 @@
{ {
"default": "1", "default": "1",
"depends_on": "eval:doc.lab_test_template_type != 'Grouped'", "depends_on": "eval:doc.lab_test_template_type != 'Grouped'",
"description": "If unchecked, the item wont be appear in Sales Invoice, but can be used in group test creation. ", "description": "If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ",
"fieldname": "is_billable", "fieldname": "is_billable",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Billable", "label": "Is Billable",
@ -128,6 +129,7 @@
"mandatory_depends_on": "eval:doc.is_billable == 1" "mandatory_depends_on": "eval:doc.is_billable == 1"
}, },
{ {
"collapsible": 1,
"fieldname": "medical_coding_section", "fieldname": "medical_coding_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Medical Coding" "label": "Medical Coding"
@ -184,7 +186,7 @@
"depends_on": "eval:doc.lab_test_template_type == 'Descriptive'", "depends_on": "eval:doc.lab_test_template_type == 'Descriptive'",
"fieldname": "section_break_special", "fieldname": "section_break_special",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Descriptive" "label": "Descriptive Test"
}, },
{ {
"default": "0", "default": "0",
@ -196,7 +198,7 @@
"depends_on": "eval:doc.lab_test_template_type == 'Grouped'", "depends_on": "eval:doc.lab_test_template_type == 'Grouped'",
"fieldname": "section_break_group", "fieldname": "section_break_group",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Group" "label": "Group Tests"
}, },
{ {
"fieldname": "lab_test_groups", "fieldname": "lab_test_groups",
@ -217,7 +219,6 @@
"no_copy": 1 "no_copy": 1
}, },
{ {
"collapsible": 1,
"fieldname": "sb_sample_collection", "fieldname": "sb_sample_collection",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Sample Collection" "label": "Sample Collection"
@ -311,10 +312,14 @@
"fieldname": "descriptive_test_templates", "fieldname": "descriptive_test_templates",
"fieldtype": "Table", "fieldtype": "Table",
"options": "Descriptive Test Template" "options": "Descriptive Test Template"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
} }
], ],
"links": [], "links": [],
"modified": "2020-07-13 12:57:09.925436", "modified": "2020-07-30 14:32:40.449818",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Lab Test Template", "name": "Lab Test Template",

View File

@ -15,7 +15,8 @@ class LabTestTemplate(Document):
def validate(self): def validate(self):
if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0): if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0):
frappe.throw(_("Standard Selling Rate should be greater than zero.")) frappe.throw(_('Standard Selling Rate should be greater than zero.'))
self.validate_conversion_factor() self.validate_conversion_factor()
self.enable_disable_item() self.enable_disable_item()
@ -42,7 +43,9 @@ class LabTestTemplate(Document):
# Remove template reference from item and disable item # Remove template reference from item and disable item
if self.item: if self.item:
try: try:
frappe.delete_doc('Item', self.item) item = self.item
self.db_set('item', '')
frappe.delete_doc('Item', item)
except Exception: except Exception:
frappe.throw(_('Not permitted. Please disable the Lab Test Template')) frappe.throw(_('Not permitted. Please disable the Lab Test Template'))
@ -63,26 +66,26 @@ class LabTestTemplate(Document):
'standard_rate': self.lab_test_rate, 'standard_rate': self.lab_test_rate,
'description': self.lab_test_description 'description': self.lab_test_description
}) })
item.save() item.flags.ignore_mandatory = True
item.save(ignore_permissions=True)
def item_price_exists(self): def item_price_exists(self):
item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code}) item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code})
if item_price: if item_price:
return item_price[0][0] return item_price[0][0]
else:
return False return False
def validate_conversion_factor(self): def validate_conversion_factor(self):
if self.lab_test_template_type == "Single" and self.secondary_uom and not self.conversion_factor: if self.lab_test_template_type == 'Single' and self.secondary_uom and not self.conversion_factor:
frappe.throw(_("Conversion Factor is mandatory")) frappe.throw(_('Conversion Factor is mandatory'))
if self.lab_test_template_type == "Compound": if self.lab_test_template_type == 'Compound':
for item in self.normal_test_templates: for item in self.normal_test_templates:
if item.secondary_uom and not item.conversion_factor: if item.secondary_uom and not item.conversion_factor:
frappe.throw(_("Conversion Factor is mandatory")) frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(item.idx))
if self.lab_test_template_type == "Grouped": if self.lab_test_template_type == 'Grouped':
for group in self.lab_test_groups: for group in self.lab_test_groups:
if group.template_or_new_line == "Add New Line" and group.secondary_uom and not group.conversion_factor: if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor:
frappe.throw(_("Conversion Factor is mandatory")) frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(group.idx))
def create_item_from_template(doc): def create_item_from_template(doc):

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'template',
'transactions': [
{
'label': _('Lab Tests'),
'items': ['Lab Test']
}
]
}

View File

@ -3,5 +3,5 @@
*/ */
frappe.listview_settings['Lab Test Template'] = { frappe.listview_settings['Lab Test Template'] = {
add_fields: ['lab_test_name', 'lab_test_code', 'lab_test_rate'], add_fields: ['lab_test_name', 'lab_test_code', 'lab_test_rate'],
filters: [['disabled', '=', 0]] filters: [['disabled', '=', 'No']]
}; };

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json import json
from frappe.utils import getdate, get_time from frappe.utils import getdate, get_time, flt
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
import datetime import datetime
@ -45,7 +45,7 @@ class PatientAppointment(Document):
def validate_overlaps(self): def validate_overlaps(self):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=float(self.duration)) + datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql(""" overlaps = frappe.db.sql("""
select select

View File

@ -34,7 +34,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
self.assertTrue(medical_rec) self.assertTrue(medical_rec)
template = create_lab_test_template(medical_department) template = create_lab_test_template(medical_department)
lab_test = create_lab_test(template, patient) lab_test = create_lab_test(template.name, patient)
# check for lab test # check for lab test
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name}) medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name})
self.assertTrue(medical_rec) self.assertTrue(medical_rec)
@ -66,7 +66,7 @@ def create_vital_signs(appointment):
def create_lab_test_template(medical_department): def create_lab_test_template(medical_department):
if frappe.db.exists('Lab Test Template', 'Blood Test'): if frappe.db.exists('Lab Test Template', 'Blood Test'):
return 'Blood Test' return frappe.get_doc('Lab Test Template', 'Blood Test')
template = frappe.new_doc('Lab Test Template') template = frappe.new_doc('Lab Test Template')
template.lab_test_name = 'Blood Test' template.lab_test_name = 'Blood Test'
@ -76,7 +76,7 @@ def create_lab_test_template(medical_department):
template.is_billable = 1 template.is_billable = 1
template.lab_test_rate = 2000 template.lab_test_rate = 2000
template.save() template.save()
return template.name return template
def create_lab_test(template, patient): def create_lab_test(template, patient):
lab_test = frappe.new_doc('Lab Test') lab_test = frappe.new_doc('Lab Test')

View File

@ -44,11 +44,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-01-31 12:21:45.975488", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Practitioner Schedule", "name": "Practitioner Schedule",
"owner": "rmehta@gmail.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"create": 1, "create": 1,

View File

@ -3,19 +3,19 @@
frappe.ui.form.on('Sample Collection', { frappe.ui.form.on('Sample Collection', {
refresh: function(frm) { refresh: function(frm) {
if(frappe.defaults.get_default("create_sample_collection_for_lab_test")){ if (frappe.defaults.get_default('create_sample_collection_for_lab_test')) {
frm.add_custom_button(__("View Lab Tests"), function() { frm.add_custom_button(__('View Lab Tests'), function() {
frappe.route_options = {"sample": frm.doc.name}; frappe.route_options = {'sample': frm.doc.name};
frappe.set_route("List", "Lab Test"); frappe.set_route('List', 'Lab Test');
}); });
} }
} }
}); });
frappe.ui.form.on("Sample Collection", "patient", function(frm) { frappe.ui.form.on('Sample Collection', 'patient', function(frm) {
if(frm.doc.patient){ if(frm.doc.patient){
frappe.call({ frappe.call({
"method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail',
args: { args: {
patient: frm.doc.patient patient: frm.doc.patient
}, },
@ -24,8 +24,8 @@ frappe.ui.form.on("Sample Collection", "patient", function(frm) {
if (data.message.dob){ if (data.message.dob){
age = calculate_age(data.message.dob); age = calculate_age(data.message.dob);
} }
frappe.model.set_value(frm.doctype,frm.docname, "patient_age", age); frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
frappe.model.set_value(frm.doctype,frm.docname, "patient_sex", data.message.sex); frappe.model.set_value(frm.doctype,frm.docname, 'patient_sex', data.message.sex);
} }
}); });
} }
@ -36,5 +36,5 @@ var calculate_age = function(birth) {
var age = new Date(); var age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
var years = age.getFullYear() - 1970; var years = age.getFullYear() - 1970;
return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)"; return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
}; };

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