Merge branch 'develop' into handle-irs

This commit is contained in:
Marica 2020-09-08 12:03:45 +05:30 committed by GitHub
commit c8eca9d5f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 2058 additions and 592 deletions

View File

@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Bank Statement", "label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -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-03 10:37:07.865801",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

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

@ -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, ignore_permissions=False):
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, ignore_permissions=ignore_permissions)
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

@ -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

@ -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

@ -447,7 +447,7 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "po_no", "fieldname": "po_no",
"fieldtype": "Small Text", "fieldtype": "Data",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Customer's Purchase Order", "label": "Customer's Purchase Order",
@ -1946,7 +1946,7 @@
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-03 23:31:12.675040", "modified": "2020-08-27 01:56:28.532140",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -3,6 +3,22 @@
frappe.ui.form.on('Shipping Rule', { frappe.ui.form.on('Shipping Rule', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.trigger('toggle_reqd'); frm.trigger('toggle_reqd');
}, },
calculate_based_on: function(frm) { calculate_based_on: function(frm) {

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
@ -342,7 +343,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

@ -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

@ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
for d in reversed(accounts): for d in reversed(accounts):
if d.parent_account: if d.parent_account:
account = d.parent_account.split('-')[0].strip() account = d.parent_account.split(' - ')[0].strip()
if not accounts_by_name.get(account): if not accounts_by_name.get(account):
continue continue

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

@ -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

@ -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

@ -276,6 +276,9 @@ class BuyingController(StockController):
qty_to_be_received_map = get_qty_to_be_received(purchase_orders) qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'): for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost # reset raw_material cost
item.rm_supp_cost = 0 item.rm_supp_cost = 0
@ -288,6 +291,12 @@ class BuyingController(StockController):
fg_yet_to_be_received = qty_to_be_received_map.get(item_key) fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)

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

@ -9,6 +9,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from erpnext.controllers.accounts_controller import validate_conversion_rate, \ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
@ -607,7 +608,7 @@ class calculate_taxes_and_totals(object):
base_rate_with_margin = 0.0 base_rate_with_margin = 0.0
if item.price_list_rate: if item.price_list_rate:
if item.pricing_rules and not self.doc.ignore_pricing_rule: if item.pricing_rules and not self.doc.ignore_pricing_rule:
for d in json.loads(item.pricing_rules): for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\

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

@ -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

@ -226,7 +226,9 @@ let check_and_set_availability = function(frm) {
primary_action_label: __('Book'), primary_action_label: __('Book'),
primary_action: function() { primary_action: function() {
frm.set_value('appointment_time', selected_slot); frm.set_value('appointment_time', selected_slot);
if (!frm.doc.duration) {
frm.set_value('duration', duration); frm.set_value('duration', duration);
}
frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('practitioner', d.get_value('practitioner'));
frm.set_value('department', d.get_value('department')); frm.set_value('department', d.get_value('department'));
frm.set_value('appointment_date', d.get_value('appointment_date')); frm.set_value('appointment_date', d.get_value('appointment_date'));

View File

@ -98,7 +98,8 @@ def add_attendance(events, start, end, conditions=None):
e = { e = {
"name": d.name, "name": d.name,
"doctype": "Attendance", "doctype": "Attendance",
"date": d.attendance_date, "start": d.attendance_date,
"end": d.attendance_date,
"title": cstr(d.status), "title": cstr(d.status),
"docstatus": d.docstatus "docstatus": d.docstatus
} }

View File

@ -1,12 +1,6 @@
// 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
frappe.views.calendar["Attendance"] = { frappe.views.calendar["Attendance"] = {
field_map: {
"start": "attendance_date",
"end": "attendance_date",
"id": "name",
"docstatus": 1
},
options: { options: {
header: { header: {
left: 'prev,next today', left: 'prev,next today',

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2019-05-09 15:47:39.760406", "creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -54,6 +53,7 @@
{ {
"fieldname": "transaction_type", "fieldname": "transaction_type",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "Transaction Type", "label": "Transaction Type",
"options": "DocType" "options": "DocType"
}, },
@ -109,9 +109,9 @@
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "modified": "2020-09-04 12:16:36.569066",
"modified": "2020-02-27 14:40:10.502605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Ledger Entry", "name": "Leave Ledger Entry",

View File

@ -0,0 +1,13 @@
frappe.listview_settings['Leave Ledger Entry'] = {
onload: function(listview) {
if(listview.page.fields_dict.transaction_type) {
listview.page.fields_dict.transaction_type.get_query = function() {
return {
"filters": {
"name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]],
}
};
};
}
}
};

View File

@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None):
"doctype": "Shift Assignment", "doctype": "Shift Assignment",
"start_date": d.start_date, "start_date": d.start_date,
"end_date": d.end_date if d.end_date else nowdate(), "end_date": d.end_date if d.end_date else nowdate(),
"title": cstr(d.employee_name) + \ "title": cstr(d.employee_name) + ": "+ \
cstr(d.shift_type), cstr(d.shift_type),
"docstatus": d.docstatus "docstatus": d.docstatus
} }

View File

@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', {
loan_type: function(frm) { loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); frm.toggle_display("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); frm.toggle_display("repayment_periods", frm.doc.is_term_loan);
}, },
@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', {
create_loan_security_unpledge: function(frm) { create_loan_security_unpledge: function(frm) {
frappe.call({ frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",
args : { args : {
"loan": frm.doc.name, "loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type, "as_dict": 1
"applicant": frm.doc.applicant,
"company": frm.doc.company
}, },
callback: function(r) { callback: function(r) {
if (r.message) if (r.message)

View File

@ -7,7 +7,7 @@ import frappe, math, json
import erpnext import erpnext
from frappe import _ from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController): class Loan(AccountsController):
@ -223,29 +223,55 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
return repayment_entry return repayment_entry
@frappe.whitelist() @frappe.whitelist()
def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
loan_security_pledge_details = frappe.db.sql(""" # if loan is passed it will be considered as full unpledge
SELECT p.loan_security, sum(p.qty) as qty if loan:
FROM `tabLoan Security Pledge` lsp , `tabPledge` p pledge_qty_map = get_pledged_security_qty(loan)
WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 loan_doc = frappe.get_doc('Loan', loan)
GROUP BY p.loan_security unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
""",(loan), as_dict=1) loan_doc.applicant_type, loan_doc.applicant)
# will unpledge qty based on loan security pledge
elif loan_security_pledge:
security_map = {}
pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge)
for security in pledge_doc.securities:
security_map.setdefault(security.loan_security, security.qty)
unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan,
pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant)
if save:
unpledge_request.save()
if submit:
unpledge_request.submit()
if approve:
if unpledge_request.docstatus == 1:
unpledge_request.status = 'Approved'
unpledge_request.save()
else:
frappe.throw(_('Only submittted unpledge requests can be approved'))
if as_dict:
return unpledge_request
else:
return unpledge_request
def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant):
unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type unpledge_request.applicant_type = applicant_type
unpledge_request.applicant = applicant unpledge_request.applicant = applicant
unpledge_request.loan = loan unpledge_request.loan = loan
unpledge_request.company = company unpledge_request.company = company
for loan_security in loan_security_pledge_details: for security, qty in unpledge_map.items():
if qty:
unpledge_request.append('securities', { unpledge_request.append('securities', {
"loan_security": loan_security.loan_security, "loan_security": security,
"qty": loan_security.qty "qty": qty
}) })
if as_dict:
return unpledge_request.as_dict()
else:
return unpledge_request return unpledge_request

View File

@ -14,9 +14,11 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
process_loan_interest_accrual_for_term_loans) process_loan_interest_accrual_for_term_loans)
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge from erpnext.loan_management.doctype.loan.loan import unpledge_security
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class TestLoan(unittest.TestCase): class TestLoan(unittest.TestCase):
def setUp(self): def setUp(self):
@ -193,18 +195,13 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit() repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
'paid_principal_amount'])
unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2))
/ (days_in_year(get_datetime(first_date).year) * 100)
self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3),
flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
loan.load_from_db() loan.load_from_db()
@ -306,13 +303,10 @@ class TestLoan(unittest.TestCase):
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit() repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db() loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) unpledge_request = unpledge_security(loan=loan.name, save=1)
unpledge_request.submit() unpledge_request.submit()
unpledge_request.status = 'Approved' unpledge_request.status = 'Approved'
unpledge_request.save() unpledge_request.save()
@ -323,6 +317,97 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, 'Closed') self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0) self.assertEquals(sum(pledged_qty.values()), 0)
def test_disbursal_check_with_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
where loan_security='Test Security 2'""")
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
self.assertTrue(loan_security_shortfall)
self.assertEqual(get_disbursal_amount(loan.name), 0)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
where loan_security='Test Security 2'""")
def test_disbursal_check_without_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
self.assertEqual(get_disbursal_amount(loan.name), 300000)
def test_pending_loan_amount_after_closure_request(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
self.assertEquals(amounts['pending_principal_amount'], 0.0)
def create_loan_accounts(): def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):

View File

@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', {
if (frm.doc.is_secured_loan) { if (frm.doc.is_secured_loan) {
frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) { if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan Security Pledge'), function() { frm.add_custom_button(__('Loan Security Pledge'), function() {
frm.trigger('create_loan_security_pledge') frm.trigger('create_loan_security_pledge');
},__('Create')) },__('Create'))
} }
}); });
} }
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) { if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan'), function() { frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan') frm.trigger('create_loan');
},__('Create')) },__('Create'))
} else { } else {
frm.set_df_property('status', 'read_only', 1); frm.set_df_property('status', 'read_only', 1);
@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', {
}, },
create_loan: function(frm) { create_loan: function(frm) {
if (frm.doc.status != "Approved") { if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved")) frappe.throw(__("Cannot create loan until application is approved"));
} }
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({

View File

@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment total_payment = loan_details.total_payment
if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: possible_disbursal_amount = get_disbursal_amount(self.against_loan)
frappe.throw(_("Disbursed Amount cannot be greater than loan amount"))
if loan_details.status == 'Disbursed': if self.disbursed_amount > possible_disbursal_amount:
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = loan_details.disbursed_amount
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(self.against_loan)
if not security_value:
security_value = loan_details.loan_amount
if pending_principal_amount + self.disbursed_amount > flt(security_value):
allowed_amount = security_value - pending_principal_amount
if allowed_amount < 0:
allowed_amount = 0
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan: if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan):
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
return security_value return security_value
@frappe.whitelist()
def get_disbursal_amount(loan):
loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"],
filters= { "name": loan })[0]
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
return 0
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = flt(loan_details.disbursed_amount)
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(loan)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
return disbursal_amount

View File

@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
if no_of_days <= 0: if no_of_days <= 0:
return return
if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.total_principal_paid)
else:
pending_principal_amount = loan.disbursed_amount
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
payable_interest = interest_per_day * no_of_days payable_interest = interest_per_day * no_of_days
@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
query_filters = { query_filters = {
"status": "Disbursed", "status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1 "docstatus": 1
} }
@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
if not open_loans: if not open_loans:
open_loans = frappe.get_all("Loan", open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
filters=query_filters) filters=query_filters)
for loan in open_loans: for loan in open_loans:
@ -209,7 +213,8 @@ def get_last_accural_date_in_current_month(loan):
WHERE loan = %s""", (loan.name)) WHERE loan = %s""", (loan.name))
if last_posting_date[0][0]: if last_posting_date[0][0]:
return last_posting_date[0][0] # interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
else: else:
return loan.disbursement_date return loan.disbursement_date

View File

@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
class LoanRepayment(AccountsController): class LoanRepayment(AccountsController):
@ -22,6 +23,9 @@ class LoanRepayment(AccountsController):
self.validate_amount() self.validate_amount()
self.allocate_amounts(amounts['pending_accrual_entries']) self.allocate_amounts(amounts['pending_accrual_entries'])
def before_submit(self):
self.book_unaccrued_interest()
def on_submit(self): def on_submit(self):
self.update_paid_amount() self.update_paid_amount()
self.make_gl_entries() self.make_gl_entries()
@ -72,6 +76,26 @@ class LoanRepayment(AccountsController):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg) frappe.throw(msg)
def book_unaccrued_interest(self):
if self.payment_type == 'Loan Closure':
total_interest_paid = 0
for payment in self.repayment_details:
total_interest_paid += payment.paid_interest_amount
if total_interest_paid < self.interest_payable:
if not self.is_term_loan:
process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date,
loan=self.against_loan)
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
self.append('repayment_details', {
'loan_interest_accrual': lia.name,
'paid_interest_amount': lia.interest_amount,
'paid_principal_amount': lia.payable_principal_amount
})
def update_paid_amount(self): def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -116,6 +140,7 @@ class LoanRepayment(AccountsController):
def allocate_amounts(self, paid_entries): def allocate_amounts(self, paid_entries):
self.set('repayment_details', []) self.set('repayment_details', [])
self.principal_amount_paid = 0 self.principal_amount_paid = 0
total_interest_paid = 0
interest_paid = self.amount_paid - self.penalty_amount interest_paid = self.amount_paid - self.penalty_amount
if self.amount_paid - self.penalty_amount > 0 and paid_entries: if self.amount_paid - self.penalty_amount > 0 and paid_entries:
@ -137,12 +162,17 @@ class LoanRepayment(AccountsController):
interest_paid = 0 interest_paid = 0
paid_principal=0 paid_principal=0
total_interest_paid += interest_amount
self.append('repayment_details', { self.append('repayment_details', {
'loan_interest_accrual': lia, 'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount, 'paid_interest_amount': interest_amount,
'paid_principal_amount': paid_principal 'paid_principal_amount': paid_principal
}) })
if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable:
unaccrued_interest = self.interest_payable - total_interest_paid
interest_paid -= unaccrued_interest
if interest_paid: if interest_paid:
self.principal_amount_paid += interest_paid self.principal_amount_paid += interest_paid
@ -297,7 +327,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
if not final_due_date: if not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'):
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
else:
pending_principal_amount = against_loan_doc.disbursed_amount
if payment_type == "Loan Closure": if payment_type == "Loan Closure":
if due_date: if due_date:

View File

@ -21,6 +21,10 @@
"total_security_value", "total_security_value",
"column_break_11", "column_break_11",
"maximum_loan_value", "maximum_loan_value",
"more_information_section",
"reference_no",
"column_break_18",
"description",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -129,11 +133,34 @@
"label": "Applicant Type", "label": "Applicant Type",
"options": "Employee\nMember\nCustomer", "options": "Employee\nMember\nCustomer",
"reqd": 1 "reqd": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-02 23:38:24.002382", "modified": "2020-09-04 22:38:19.894488",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Pledge", "name": "Loan Security Pledge",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import get_datetime from frappe.utils import get_datetime, flt
from frappe.model.document import Document from frappe.model.document import Document
from six import iteritems from six import iteritems
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
"valid_upto": (">=", update_time) "valid_upto": (">=", update_time)
}, as_list=1)) }, as_list=1))
loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment',
filters={'status': 'Disbursed', 'is_secured_loan': 1}) 'total_interest_payable', 'disbursed_amount', 'status'],
filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1})
loan_security_map = {} loan_security_map = {}
for loan in loans: for loan in loans:
outstanding_amount = loan.loan_amount - loan.total_principal_paid if loan.status == 'Disbursed':
outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
outstanding_amount = loan.disbursed_amount
pledged_securities = get_pledged_security_qty(loan.name) pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = '' ltv_ratio = ''
security_value = 0.0 security_value = 0.0

View File

@ -16,6 +16,10 @@
"status", "status",
"loan_security_details_section", "loan_security_details_section",
"securities", "securities",
"more_information_section",
"reference_no",
"column_break_13",
"description",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -95,11 +99,34 @@
"label": "Applicant Type", "label": "Applicant Type",
"options": "Employee\nMember\nCustomer", "options": "Employee\nMember\nCustomer",
"reqd": 1 "reqd": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-05 07:23:18.440058", "modified": "2020-09-04 22:39:57.756146",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Unpledge", "name": "Loan Security Unpledge",

View File

@ -17,7 +17,6 @@ class LoanSecurityUnpledge(Document):
self.validate_unpledge_qty() self.validate_unpledge_qty()
def on_cancel(self): def on_cancel(self):
self.update_loan_security_pledge(cancel=1)
self.update_loan_status(cancel=1) self.update_loan_status(cancel=1)
self.db_set('status', 'Requested') self.db_set('status', 'Requested')
@ -43,13 +42,14 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime()) "valid_upto": (">=", get_datetime())
}, as_list=1)) }, as_list=1))
loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid']) total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
pending_principal_amount = loan_amount - principal_paid 'total_interest_payable'])
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid)
security_value = 0 security_value = 0
for security in self.securities: for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security) pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty: if security.qty > pledged_qty:
frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
@ -58,16 +58,23 @@ class LoanSecurityUnpledge(Document):
qty_after_unpledge = pledged_qty - security.qty qty_after_unpledge = pledged_qty - security.qty
ltv_ratio = ltv_ratio_map.get(security.loan_security_type) ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) current_price = loan_security_price_map.get(security.loan_security)
if not current_price:
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security)))
if not security_value and pending_principal_amount > 0: security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching") frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching") frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
def on_update_after_submit(self): def on_update_after_submit(self):
if self.status == "Approved": self.approve()
def approve(self):
if self.status == "Approved" and not self.unpledge_time:
self.update_loan_status() self.update_loan_status()
self.db_set('unpledge_time', get_datetime()) self.db_set('unpledge_time', get_datetime())

View File

@ -36,6 +36,8 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=
loan_process.submit() loan_process.submit()
return loan_process.name
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()): if not term_loan_accrual_pending(posting_date or nowdate()):
@ -49,6 +51,8 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No
loan_process.submit() loan_process.submit()
return loan_process.name
def term_loan_accrual_pending(date): def term_loan_accrual_pending(date):
pending_accrual = frappe.db.get_value('Repayment Schedule', { pending_accrual = frappe.db.get_value('Repayment Schedule', {
'payment_date': ('<=', date), 'payment_date': ('<=', date),

View File

@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase):
for key in scheduled_date: for key in scheduled_date:
description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
frappe.get_doc({ event = frappe.get_doc({
"doctype": "Event", "doctype": "Event",
"owner": email_map.get(d.sales_person, self.owner), "owner": email_map.get(d.sales_person, self.owner),
"subject": description, "subject": description,
"description": description, "description": description,
"starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
"event_type": "Private", "event_type": "Private",
"ref_type": self.doctype, })
"ref_name": self.name event.add_participant(self.doctype, self.name)
}).insert(ignore_permissions=1) event.insert(ignore_permissions=1)
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')

View File

@ -2,6 +2,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.utils.data import get_datetime, add_days
import frappe import frappe
import unittest import unittest
@ -9,4 +10,39 @@ import unittest
# test_records = frappe.get_test_records('Maintenance Schedule') # test_records = frappe.get_test_records('Maintenance Schedule')
class TestMaintenanceSchedule(unittest.TestCase): class TestMaintenanceSchedule(unittest.TestCase):
pass def test_events_should_be_created_and_deleted(self):
ms = make_maintenance_schedule()
ms.generate_schedule()
ms.submit()
all_events = get_events(ms)
self.assertTrue(len(all_events) > 0)
ms.cancel()
events_after_cancel = get_events(ms)
self.assertTrue(len(events_after_cancel) == 0)
def get_events(ms):
return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype,
"reference_docname": ms.name,
"parenttype": "Event"
})
def make_maintenance_schedule():
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = get_datetime()
ms.append("items", {
"item_code": "_Test Item",
"start_date": get_datetime(),
"end_date": add_days(get_datetime(), 32),
"periodicity": "Weekly",
"no_of_visits": 4,
"sales_person": "Sales Team",
})
ms.insert(ignore_permissions=True)
return ms

View File

@ -90,6 +90,7 @@ def update_latest_price_in_all_boms():
update_cost() update_cost()
def replace_bom(args): def replace_bom(args):
frappe.db.auto_commit_on_many_writes = 1
args = frappe._dict(args) args = frappe._dict(args)
doc = frappe.get_doc("BOM Update Tool") doc = frappe.get_doc("BOM Update Tool")
@ -97,6 +98,8 @@ def replace_bom(args):
doc.new_bom = args.new_bom doc.new_bom = args.new_bom
doc.replace_bom() doc.replace_bom()
frappe.db.auto_commit_on_many_writes = 0
def update_cost(): def update_cost():
frappe.db.auto_commit_on_many_writes = 1 frappe.db.auto_commit_on_many_writes = 1
bom_list = get_boms_in_bottom_up_order() bom_list = get_boms_in_bottom_up_order()

View File

@ -2,6 +2,17 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Job Card', { frappe.ui.form.on('Job Card', {
setup: function(frm) {
frm.set_query('operation', function() {
return {
query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations',
filters: {
'work_order': frm.doc.work_order
}
};
});
},
refresh: function(frm) { refresh: function(frm) {
frappe.flags.pause_job = 0; frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0; frappe.flags.resume_job = 0;
@ -20,12 +31,60 @@ frappe.ui.form.on('Job Card', {
} }
} }
frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons"); frm.trigger("prepare_timer_buttons");
} }
}, },
operation: function(frm) {
frm.trigger("toggle_operation_number");
if (frm.doc.operation && frm.doc.work_order) {
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details",
args: {
"work_order":frm.doc.work_order,
"operation":frm.doc.operation
},
callback: function (r) {
if (r.message) {
if (r.message.length == 1) {
frm.set_value("operation_id", r.message[0].name);
} else {
let args = [];
r.message.forEach((row) => {
args.push({ "label": row.idx, "value": row.name });
});
let description = __("Operation {0} added multiple times in the work order {1}",
[frm.doc.operation, frm.doc.work_order]);
frm.set_df_property("operation_row_number", "options", args);
frm.set_df_property("operation_row_number", "description", description);
}
frm.trigger("toggle_operation_number");
}
}
})
}
},
operation_row_number(frm) {
if (frm.doc.operation_row_number) {
frm.set_value("operation_id", frm.doc.operation_row_number);
}
},
toggle_operation_number(frm) {
frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
},
prepare_timer_buttons: function(frm) { prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard"); frm.trigger("make_dashboard");
if (!frm.doc.job_started) { if (!frm.doc.job_started) {
@ -35,9 +94,9 @@ frappe.ui.form.on('Job Card', {
fieldname: 'employee'}, d => { fieldname: 'employee'}, d => {
if (d.employee) { if (d.employee) {
frm.set_value("employee", d.employee); frm.set_value("employee", d.employee);
} } else {
frm.events.start_job(frm); frm.events.start_job(frm);
}
}, __("Enter Value"), __("Start")); }, __("Enter Value"), __("Start"));
} else { } else {
frm.events.start_job(frm); frm.events.start_job(frm);
@ -82,9 +141,7 @@ frappe.ui.form.on('Job Card', {
frm.set_value('current_time' , 0); frm.set_value('current_time' , 0);
} }
frm.save("Save", () => {}, "", () => { frm.save();
frm.doc.time_logs.pop(-1);
});
}, },
complete_job: function(frm, completed_time, completed_qty) { complete_job: function(frm, completed_time, completed_qty) {
@ -116,6 +173,8 @@ frappe.ui.form.on('Job Card', {
employee: function(frm) { employee: function(frm) {
if (frm.doc.job_started && !frm.doc.current_time) { if (frm.doc.job_started && !frm.doc.current_time) {
frm.trigger("reset_timer"); frm.trigger("reset_timer");
} else {
frm.events.start_job(frm);
} }
}, },

View File

@ -11,6 +11,7 @@
"bom_no", "bom_no",
"workstation", "workstation",
"operation", "operation",
"operation_row_number",
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"company", "company",
@ -291,11 +292,15 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "operation_row_number",
"fieldtype": "Select",
"label": "Operation Row Number"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "modified": "2020-08-24 15:21:21.398267",
"modified": "2020-04-20 15:14:00.273441",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",
@ -347,7 +352,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "operation", "title_field": "operation",

View File

@ -15,10 +15,13 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class OperationMismatchError(frappe.ValidationError): pass
class JobCard(Document): class JobCard(Document):
def validate(self): def validate(self):
self.validate_time_logs() self.validate_time_logs()
self.set_status() self.set_status()
self.validate_operation_id()
def validate_time_logs(self): def validate_time_logs(self):
self.total_completed_qty = 0.0 self.total_completed_qty = 0.0
@ -209,11 +212,10 @@ class JobCard(Document):
for_quantity, time_in_mins = 0, 0 for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
field = "operation_id" if self.operation_id else "operation" field = "operation_id"
data = frappe.get_all('Job Card', data = frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)})
"workstation": self.workstation, field: self.get(field)})
if data and len(data) > 0: if data and len(data) > 0:
for_quantity = data[0].completed_qty for_quantity = data[0].completed_qty
@ -226,14 +228,13 @@ class JobCard(Document):
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s
and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 and jc.{0} = %s and jc.docstatus = 1
""".format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) """.format(field), (self.work_order, self.get(field)), as_dict=1)
wo = frappe.get_doc('Work Order', self.work_order) wo = frappe.get_doc('Work Order', self.work_order)
work_order_field = "name" if field == "operation_id" else field
for data in wo.operations: for data in wo.operations:
if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: if data.get("name") == self.get(field):
data.completed_qty = for_quantity data.completed_qty = for_quantity
data.actual_operation_time = time_in_mins data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None data.actual_start_time = time_data[0].start_time if time_data else None
@ -306,6 +307,37 @@ class JobCard(Document):
if update_status: if update_status:
self.db_set('status', self.status) self.db_set('status', self.status)
def validate_operation_id(self):
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
work_order = frappe.bold(get_link_to_form("Work Order", self.work_order))
frappe.throw(_("Operation {0} does not belong to the work order {1}")
.format(frappe.bold(self.operation), work_order), OperationMismatchError)
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
return frappe.get_all("Work Order Operation", fields = ["name", "idx"],
filters = {
"parent": work_order,
"operation": operation
}
)
@frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters):
if filters.get("work_order"):
args = {"parent": filters.get("work_order")}
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
return frappe.get_all("Work Order Operation",
filters = args,
fields = ["distinct operation as operation"],
limit_start = start,
limit_page_length = page_len,
order_by="idx asc", as_list=1)
@frappe.whitelist() @frappe.whitelist()
def make_material_request(source_name, target_doc=None): def make_material_request(source_name, target_doc=None):
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):

View File

@ -4,6 +4,72 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe
from frappe.utils import random_string
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError
class TestJobCard(unittest.TestCase): class TestJobCard(unittest.TestCase):
pass def test_job_card(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name}, fields = ["operation_id", "name"])
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
job_card = job_cards[0]
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
for d in job_cards:
frappe.delete_doc("Job Card", d.name)

View File

@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase):
"_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00")
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
def make_workstation(**args):
args = frappe._dict(args)
try:
doc = frappe.get_doc({
"doctype": "Workstation",
"workstation_name": args.workstation_name
})
doc.insert()
return doc
except frappe.DuplicateEntryError:
return frappe.get_doc("Workstation", args.workstation_name)

View File

@ -133,7 +133,8 @@
{ {
"fieldname": "email_id", "fieldname": "email_id",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Email Address" "label": "Email Address",
"options": "Email"
}, },
{ {
"fieldname": "subscription_id", "fieldname": "subscription_id",
@ -176,7 +177,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-04-07 14:20:33.215700", "modified": "2020-08-06 10:06:01.153564",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Member", "name": "Member",

View File

@ -9,6 +9,7 @@ from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint from frappe.utils import cint
from frappe.integrations.utils import get_payment_gateway_controller from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
class Member(Document): class Member(Document):
def onload(self): def onload(self):
@ -74,19 +75,23 @@ def get_or_create_member(user_details):
return create_member(user_details) return create_member(user_details)
def create_member(user_details): def create_member(user_details):
user_details = frappe._dict(user_details)
member = frappe.new_doc("Member") member = frappe.new_doc("Member")
member.update({ member.update({
"member_name": user_details.fullname, "member_name": user_details.fullname,
"email_id": user_details.email, "email_id": user_details.email,
"pan_number": user_details.pan, "pan_number": user_details.pan or None,
"membership_type": user_details.plan_id, "membership_type": user_details.plan_id,
"customer": create_customer(user_details) "subscription_id": user_details.subscription_id or None
}) })
member.insert(ignore_permissions=True) member.insert(ignore_permissions=True)
member.customer = create_customer(user_details, member.name)
member.save(ignore_permissions=True)
return member return member
def create_customer(user_details): def create_customer(user_details, member=None):
customer = frappe.new_doc("Customer") customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname customer.customer_name = user_details.fullname
customer.customer_type = "Individual" customer.customer_type = "Individual"
@ -107,7 +112,13 @@ def create_customer(user_details):
"link_name": customer.name "link_name": customer.name
}) })
contact.save() if member:
contact.append("links", {
"link_doctype": "Member",
"link_name": member
})
contact.save(ignore_permissions=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
return customer.name return customer.name
@ -139,8 +150,6 @@ def create_member_subscription_order(user_details):
user_details = frappe._dict(user_details) user_details = frappe._dict(user_details)
member = get_or_create_member(user_details) member = get_or_create_member(user_details)
if not member:
member = create_member(user_details)
subscription = member.setup_subscription() subscription = member.setup_subscription()
@ -148,3 +157,24 @@ def create_member_subscription_order(user_details):
member.save(ignore_permissions=True) member.save(ignore_permissions=True)
return subscription return subscription
@frappe.whitelist()
def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None):
plan = get_membership_type(rzpay_plan_id)
if not plan:
raise frappe.DoesNotExistError
member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id })
if member:
return member
else:
member = create_member(dict(
fullname=fullname,
email=email,
plan_id=plan,
subscription_id=subscription_id,
pan=pan,
mobile=mobile
))
return member.name

View File

@ -8,6 +8,24 @@ frappe.ui.form.on('Membership', {
}) })
}, },
refresh: function(frm) {
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", {
save: true
}).then(() => {
frm.reload_doc();
});
});
frappe.db.get_single_value("Membership Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => {
frm.reload_doc();
});
});
})
},
onload: function(frm) { onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount'); frm.add_fetch('membership_type', 'amount', 'amount');
} }

View File

@ -19,10 +19,10 @@
"paid", "paid",
"currency", "currency",
"amount", "amount",
"invoice",
"razorpay_details_section", "razorpay_details_section",
"subscription_id", "subscription_id",
"payment_id", "payment_id"
"webhook_payload"
], ],
"fields": [ "fields": [
{ {
@ -118,17 +118,15 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "webhook_payload", "fieldname": "invoice",
"fieldtype": "Code", "fieldtype": "Link",
"hidden": 1, "label": "Invoice",
"label": "Webhook Payload", "options": "Sales Invoice"
"options": "JSON",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-07-27 14:28:11.532696", "modified": "2020-07-31 13:57:02.328995",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership", "name": "Membership",

View File

@ -10,6 +10,7 @@ from datetime import datetime
from frappe.model.document import Document from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers from frappe.email import sendmail_to_system_managers
from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form
from erpnext.non_profit.doctype.member.member import create_member
from frappe import _ from frappe import _
import erpnext import erpnext
@ -57,11 +58,95 @@ class Membership(Document):
self.load_from_db() self.load_from_db()
self.db_set('paid', 1) self.db_set('paid', 1)
def generate_invoice(self, save=True):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
if self.invoice:
frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
if not member.customer:
frappe.throw(_("No customer linked to member {}", [member.name]))
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
if save:
self.save()
return invoice
def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings")
if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings"))
member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
email = member.email_id if member.email_id else member.email
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice:
attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format))
email_template = frappe.get_doc("Email Template", settings.email_template)
context = { "doc": self, "member": member}
email_args = {
"recipients": [email],
"message": frappe.render_template(email_template.get("response"), context),
"subject": frappe.render_template(email_template.get("subject"), context),
"attachments": attachments,
"reference_doctype": self.doctype,
"reference_name": self.name
}
if not frappe.flags.in_test:
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
else:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
invoice = self.generate_invoice(False)
self.send_acknowlement()
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
'doctype': 'Sales Invoice',
'customer': member.customer,
'debit_to': settings.debit_account,
'currency': membership.currency,
'is_pos': 0,
'items': [
{
'item_code': plan.linked_item,
'rate': membership.amount,
'qty': 1
}
]
})
invoice.insert(ignore_permissions=True)
invoice.submit()
return invoice
def get_member_based_on_subscription(subscription_id, email): def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={ members = frappe.get_all("Member", filters={
'subscription_id': subscription_id, 'subscription_id': subscription_id,
'email_id': email 'email_id': email
}, order_by="creation desc") }, order_by="creation desc")
try: try:
return frappe.get_doc("Member", members[0]['name']) return frappe.get_doc("Member", members[0]['name'])
except: except:
@ -77,16 +162,15 @@ def verify_signature(data):
controller.verify_signature(data, signature, key) controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs): def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True) data = frappe.request.get_data(as_text=True)
try: try:
verify_signature(data) verify_signature(data)
except Exception as e: except Exception as e:
signature = frappe.request.headers.get('X-Razorpay-Signature') log = frappe.log_error(e, "Webhook Verification Error")
log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) notify_failure(log)
frappe.log_error(e, "Webhook Verification Error") return { 'status': 'Failed', 'reason': e}
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
@ -99,19 +183,27 @@ def trigger_razorpay_subscription(*args, **kwargs):
payment = frappe._dict(payment) payment = frappe._dict(payment)
try: try:
data_json = json.dumps(data, indent=4, sort_keys=True) if not data.event == "subscription.charged":
member = get_member_based_on_subscription(subscription.id, payment.email) return
except Exception as e:
error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed"))
notify_failure(error_log)
return { status: 'Failed' }
member = get_member_based_on_subscription(subscription.id, payment.email)
if not member: if not member:
return { status: 'Failed' } member = create_member(frappe._dict({
try: 'fullname': payment.email,
if data.event == "subscription.activated": 'email': payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
}))
member.subscription_id = subscription.id
member.customer_id = payment.customer_id member.customer_id = payment.customer_id
elif data.event == "subscription.charged": if subscription.notes and type(subscription.notes) == dict:
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
# Update Membership
membership = frappe.new_doc("Membership") membership = frappe.new_doc("Membership")
membership.update({ membership.update({
"member": member.name, "member": member.name,
@ -120,14 +212,13 @@ def trigger_razorpay_subscription(*args, **kwargs):
"currency": "INR", "currency": "INR",
"paid": 1, "paid": 1,
"payment_id": payment.id, "payment_id": payment.id,
"webhook_payload": data_json,
"from_date": datetime.fromtimestamp(subscription.current_start), "from_date": datetime.fromtimestamp(subscription.current_start),
"to_date": datetime.fromtimestamp(subscription.current_end), "to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise "amount": payment.amount / 100 # Convert to rupees from paise
}) })
membership.insert(ignore_permissions=True) membership.insert(ignore_permissions=True)
# Update these values anyway # Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1 member.subscription_activated = 1
@ -135,9 +226,9 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e: except Exception as e:
log = frappe.log_error(e, "Error creating membership entry") log = frappe.log_error(e, "Error creating membership entry")
notify_failure(log) notify_failure(log)
return { status: 'Failed' } return { 'status': 'Failed', 'reason': e}
return { status: 'Success' } return { 'status': 'Success' }
def notify_failure(log): def notify_failure(log):
@ -152,3 +243,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except: except:
pass pass
def get_plan_from_razorpay_id(plan_id):
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc")
try:
return plan[0]['name']
except:
return None

View File

@ -10,7 +10,39 @@ frappe.ui.form.on("Membership Settings", {
}) })
}); });
} }
frm.set_query('inv_print_format', function(doc) {
return {
filters: {
"doc_type": "Sales Invoice"
}
};
});
frm.set_query('membership_print_format', function(doc) {
return {
filters: {
"doc_type": "Membership"
}
};
});
frm.set_query('debit_account', function(doc) {
return {
filters: {
'account_type': 'Receivable',
'is_group': 0,
'company': frm.doc.company
}
};
});
let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
frm.trigger("add_generate_button"); frm.trigger("add_generate_button");
frm.trigger("add_copy_buttonn");
}, },
add_generate_button: function(frm) { add_generate_button: function(frm) {
@ -27,4 +59,12 @@ frappe.ui.form.on("Membership Settings", {
}); });
}); });
}, },
add_copy_buttonn: function(frm) {
if (frm.doc.webhook_secret) {
frm.add_custom_button(__("Copy Webhook URL"), () => {
frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
});
}
}
}); });

View File

@ -9,7 +9,17 @@
"razorpay_settings_section", "razorpay_settings_section",
"billing_cycle", "billing_cycle",
"billing_frequency", "billing_frequency",
"webhook_secret" "webhook_secret",
"column_break_6",
"enable_auto_invoicing",
"company",
"debit_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template"
], ],
"fields": [ "fields": [
{ {
@ -41,11 +51,79 @@
"fieldtype": "Password", "fieldtype": "Password",
"label": "Webhook Secret", "label": "Webhook Secret",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Invoicing"
},
{
"default": "0",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Account"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Company"
},
{
"default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-05-22 12:38:27.103759", "modified": "2020-08-05 17:26:37.287395",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership Settings", "name": "Membership Settings",
@ -60,6 +138,23 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', {
refresh: function(frm) { refresh: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
}) });
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
} }
}); });

View File

@ -8,7 +8,8 @@
"field_order": [ "field_order": [
"membership_type", "membership_type",
"amount", "amount",
"razorpay_plan_id" "razorpay_plan_id",
"linked_item"
], ],
"fields": [ "fields": [
{ {
@ -33,10 +34,17 @@
"hidden": 1, "hidden": 1,
"label": "Razorpay Plan ID", "label": "Razorpay Plan ID",
"unique": 1 "unique": 1
},
{
"fieldname": "linked_item",
"fieldtype": "Link",
"label": "Linked Item",
"options": "Item",
"unique": 1
} }
], ],
"links": [], "links": [],
"modified": "2020-03-30 12:54:07.850857", "modified": "2020-08-05 15:21:43.595745",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership Type", "name": "Membership Type",

View File

@ -4,6 +4,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
import frappe
class MembershipType(Document): class MembershipType(Document):
pass pass
def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})

View File

@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field')
erpnext.patches.v12_0.remove_bank_remittance_custom_fields erpnext.patches.v12_0.remove_bank_remittance_custom_fields
erpnext.patches.v12_0.generate_leave_ledger_entries erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020
execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") execute:frappe.delete_doc_if_exists("Report", "Loan Repayment")
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
@ -718,7 +718,10 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order
erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v12_0.update_item_tax_template_company
erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.move_branch_code_to_bank_account
erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes
erpnext.patches.v13_0.add_standard_navbar_items #4
erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v13_0.stock_entry_enhancements
erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports

View File

@ -36,8 +36,7 @@ def generate_allocation_ledger_entries():
for allocation in allocation_list: for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
allocation.update(dict(doctype="Leave Allocation")) allocation_obj = frappe.get_doc("Leave Allocation", allocation)
allocation_obj = frappe.get_doc(allocation)
allocation_obj.create_leave_ledger_entry() allocation_obj.create_leave_ledger_entry()
def generate_application_leave_ledger_entries(): def generate_application_leave_ledger_entries():
@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries():
for application in leave_applications: for application in leave_applications:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
application.update(dict(doctype="Leave Application")) frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry()
frappe.get_doc(application).create_leave_ledger_entry()
def generate_encashment_leave_ledger_entries(): def generate_encashment_leave_ledger_entries():
''' fix ledger entries for missing leave encashment transaction ''' ''' fix ledger entries for missing leave encashment transaction '''
@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries():
for encashment in leave_encashments: for encashment in leave_encashments:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
encashment.update(dict(doctype="Leave Encashment")) frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry()
frappe.get_doc(encashment).create_leave_ledger_entry()
def generate_expiry_allocation_ledger_entries(): def generate_expiry_allocation_ledger_entries():
''' fix ledger entries for missing leave allocation transaction ''' ''' fix ledger entries for missing leave allocation transaction '''
@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries():
for allocation in allocation_list: for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
allocation.update(dict(doctype="Leave Allocation")) allocation_obj = frappe.get_doc("Leave Allocation", allocation)
allocation_obj = frappe.get_doc(allocation)
if allocation_obj.to_date <= getdate(today()): if allocation_obj.to_date <= getdate(today()):
expire_allocation(allocation_obj) expire_allocation(allocation_obj)
def get_allocation_records(): def get_allocation_records():
return frappe.get_all("Leave Allocation", filters={ return frappe.get_all("Leave Allocation", filters={"docstatus": 1},
"docstatus": 1 fields=['name'], order_by='to_date ASC')
}, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated',
'unused_leaves', 'from_date', 'to_date', 'carry_forward'
], order_by='to_date ASC')
def get_leaves_application_records(): def get_leaves_application_records():
return frappe.get_all("Leave Application", filters={ return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name'])
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date'])
def get_leave_encashment_records(): def get_leave_encashment_records():
return frappe.get_all("Leave Encashment", filters={ return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name'])
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date'])

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
# import frappe
from erpnext.setup.install import add_standard_navbar_items
def execute():
# Add standard navbar items for ERPNext in Navbar Settings
add_standard_navbar_items()

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists("DocType", "Membership"):
if 'webhook_payload' in frappe.db.get_table_columns("Membership"):
frappe.db.sql("alter table `tabMembership` drop column webhook_payload")

View File

@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
from erpnext.regional.india.setup import add_custom_roles_for_reports
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
add_custom_roles_for_reports()

View File

@ -25,3 +25,7 @@ def execute():
doc = frappe.new_doc('Warehouse Type') doc = frappe.new_doc('Warehouse Type')
doc.name = 'Transit' doc.name = 'Transit'
doc.insert() doc.insert()
frappe.reload_doc("stock", "doctype", "stock_entry_type")
frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse")
frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse")

View File

@ -5,8 +5,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _, bold
from frappe.utils import getdate, date_diff from frappe.utils import getdate, date_diff, comma_and, formatdate
class AdditionalSalary(Document): class AdditionalSalary(Document):
@ -22,9 +22,37 @@ class AdditionalSalary(Document):
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_recurring_additional_salary_overlap()
if self.amount < 0: if self.amount < 0:
frappe.throw(_("Amount should not be less than zero.")) frappe.throw(_("Amount should not be less than zero."))
def validate_recurring_additional_salary_overlap(self):
if self.is_recurring:
additional_salaries = frappe.db.sql("""
SELECT
name
FROM `tabAdditional Salary`
WHERE
employee=%s
AND name <> %s
AND docstatus=1
AND is_recurring=1
AND salary_component = %s
AND to_date >= %s
AND from_date <= %s""",
(self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1)
additional_salaries = [salary.name for salary in additional_salaries]
if additional_salaries and len(additional_salaries):
frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format(
bold(comma_and(additional_salaries)),
bold(self.salary_component),
bold(formatdate(self.from_date)),
bold(formatdate(self.to_date)
)))
def validate_dates(self): def validate_dates(self):
date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])

View File

@ -3,32 +3,6 @@
frappe.provide('erpnext'); frappe.provide('erpnext');
// add toolbar icon
$(document).bind('toolbar_setup', function() {
frappe.app.name = "ERPNext";
frappe.help_feedback_link = '<p><a class="text-muted" \
href="https://discuss.erpnext.com">Feedback</a></p>'
$('[data-link="docs"]').attr("href", "https://erpnext.com/docs")
$('[data-link="issues"]').attr("href", "https://github.com/frappe/erpnext/issues")
// default documentation goes to erpnext
// $('[data-link-type="documentation"]').attr('data-path', '/erpnext/manual/index');
// additional help links for erpnext
var $help_menu = $('.dropdown-help ul .documentation-links');
$('<li><a data-link-type="forum" href="https://erpnext.com/docs/user/manual" \
target="_blank">'+__('Documentation')+'</a></li>').insertBefore($help_menu);
$('<li><a data-link-type="forum" href="https://discuss.erpnext.com" \
target="_blank">'+__('User Forum')+'</a></li>').insertBefore($help_menu);
$('<li><a href="https://github.com/frappe/erpnext/issues" \
target="_blank">'+__('Report an Issue')+'</a></li>').insertBefore($help_menu);
});
// preferred modules for breadcrumbs // preferred modules for breadcrumbs
$.extend(frappe.breadcrumbs.preferred, { $.extend(frappe.breadcrumbs.preferred, {
"Item Group": "Stock", "Item Group": "Stock",

View File

@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
if(!r.exc && r.message) { if(!r.exc && r.message) {
remove_empty_first_row(frm); remove_empty_first_row(frm);
for ( var i=0; i< r.message.length; i++ ) { for (var i=0; i< r.message.length; i++) {
var d = frm.add_child("items"); var d = frm.add_child("items");
var item = r.message[i]; var item = r.message[i];
for ( var key in item) { for (var key in item) {
if ( !is_null(item[key]) ) { if (!is_null(item[key]) && key !== "doctype") {
d[key] = item[key]; d[key] = item[key];
} }
} }

View File

@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
else var date = this.frm.doc.transaction_date; else var date = this.frm.doc.transaction_date;
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){ erpnext.utils.get_shipping_address(this.frm, function(){
set_party_account(set_pricing); set_party_account(set_pricing);
}) })
// Get default company billing address in Purchase Invoice, Order and Receipt
frappe.call({
'method': 'frappe.contacts.doctype.address.address.get_default_address',
'args': {
'doctype': 'Company',
'name': this.frm.doc.company
},
'callback': function(r) {
me.frm.set_value('billing_address', r.message);
}
});
} else { } else {
set_party_account(set_pricing); set_party_account(set_pricing);
} }

View File

@ -73,6 +73,19 @@ def add_custom_roles_for_reports():
] ]
)).insert() )).insert()
for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
doctype='Custom Role',
report=report_name,
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager'),
dict(role='Auditor')
]
)).insert()
def add_permissions(): def add_permissions():
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
add_permission(doctype, 'All', 0) add_permission(doctype, 'All', 0)

View File

@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
shipping_address: function(frm) { shipping_address: function(frm) {
frm.trigger('get_tax_template'); frm.trigger('get_tax_template');
}, },
supplier_address: function(frm) {
frm.trigger('get_tax_template');
},
tax_category: function(frm) { tax_category: function(frm) {
frm.trigger('get_tax_template'); frm.trigger('get_tax_template');
}, },

View File

@ -7,7 +7,7 @@
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2019-06-30 19:33:59.769385", "modified": "2019-09-03 19:33:59.769385",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "GSTR-1", "name": "GSTR-1",
@ -16,15 +16,5 @@
"ref_doctype": "GL Entry", "ref_doctype": "GL Entry",
"report_name": "GSTR-1", "report_name": "GSTR-1",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": []
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
} }

View File

@ -7,7 +7,7 @@
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2018-01-29 12:59:55.650445", "modified": "2018-09-03 12:59:55.650445",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "GSTR-2", "name": "GSTR-2",
@ -15,15 +15,5 @@
"ref_doctype": "GL Entry", "ref_doctype": "GL Entry",
"report_name": "GSTR-2", "report_name": "GSTR-2",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": []
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
} }

View File

@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = {
], ],
onload: (report) => { onload: (report) => {
fetch_gstins(report); fetch_gstins(report);
report.page.add_inner_button(__("Download JSON"), function () {
var filters = report.get_values();
frappe.call({
method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json',
args: {
data: report.data,
report_name: report.report_name,
filters: filters
},
callback: function(r) {
if (r.message) {
const args = {
cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file',
data: r.message.data,
report_name: r.message.report_name
};
open_url_post(frappe.request.url, args);
}
}
});
});
} }
}; };

View File

@ -6,7 +6,7 @@
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2019-04-26 12:59:38.603649", "modified": "2019-09-03 12:59:38.603649",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "HSN-wise-summary of outward supplies", "name": "HSN-wise-summary of outward supplies",
@ -14,15 +14,5 @@
"ref_doctype": "Sales Invoice", "ref_doctype": "Sales Invoice",
"report_name": "HSN-wise-summary of outward supplies", "report_name": "HSN-wise-summary of outward supplies",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": []
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
} }

View File

@ -4,11 +4,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, getdate, cstr
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from six import iteritems from six import iteritems
import json import json
from erpnext.regional.india.utils import get_gst_accounts
from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number
def execute(filters=None): def execute(filters=None):
return _execute(filters) return _execute(filters)
@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
tax_details = frappe.db.sql(""" tax_details = frappe.db.sql("""
select select
parent, description, item_wise_tax_detail, parent, account_head, item_wise_tax_detail,
base_tax_amount_after_discount_amount base_tax_amount_after_discount_amount
from `tab%s` from `tab%s`
where where
@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
""" % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions),
tuple([doctype] + list(invoice_item_row))) tuple([doctype] + list(invoice_item_row)))
for parent, description, item_wise_tax_detail, tax_amount in tax_details: for parent, account_head, item_wise_tax_detail, tax_amount in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount: if account_head not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports # as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(description) tax_columns.append(account_head)
if item_wise_tax_detail: if item_wise_tax_detail:
try: try:
@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
for d in item_row_map.get(parent, {}).get(item_code, []): for d in item_row_map.get(parent, {}).get(item_code, []):
item_tax_amount = tax_amount item_tax_amount = tax_amount
if item_tax_amount: if item_tax_amount:
itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({
"tax_amount": flt(item_tax_amount, tax_amount_precision) "tax_amount": flt(item_tax_amount, tax_amount_precision)
}) })
except ValueError: except ValueError:
continue continue
tax_columns.sort() tax_columns.sort()
for desc in tax_columns: for account_head in tax_columns:
columns.append({ columns.append({
"label": desc, "label": account_head,
"fieldname": frappe.scrub(desc), "fieldname": frappe.scrub(account_head),
"fieldtype": "Float", "fieldtype": "Float",
"width": 110 "width": 110
}) })
@ -212,3 +214,76 @@ def get_merged_data(columns, data):
return result return result
@frappe.whitelist()
def get_json(filters, report_name, data):
filters = json.loads(filters)
report_data = json.loads(data)
gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"])
if not filters.get('from_date') or not filters.get('to_date'):
frappe.throw(_("Please enter From Date and To Date to generate JSON"))
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
gst_json = {"version": "GST2.3.4",
"hash": "hash", "gstin": gstin, "fp": fp}
gst_json["hsn"] = {
"data": get_hsn_wise_json_data(filters, report_data)
}
return {
'report_name': report_name,
'data': gst_json
}
@frappe.whitelist()
def download_json_file():
'''download json content in a file'''
data = frappe._dict(frappe.local.form_dict)
frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json'
frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
def get_hsn_wise_json_data(filters, report_data):
filters = frappe._dict(filters)
gst_accounts = get_gst_accounts(filters.company)
data = []
count = 1
for hsn in report_data:
row = {
"num": count,
"hsn_sc": hsn.get("gst_hsn_code"),
"desc": hsn.get("description"),
"uqc": hsn.get("stock_uom").upper(),
"qty": hsn.get("stock_qty"),
"val": flt(hsn.get("total_amount"), 2),
"txval": flt(hsn.get("taxable_amount", 2)),
"iamt": 0.0,
"camt": 0.0,
"samt": 0.0,
"csamt": 0.0
}
for account in gst_accounts.get('igst_account'):
row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('cgst_account'):
row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('sgst_account'):
row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('cess_account'):
row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
data.append(row)
count +=1
return data

View File

@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager")
# form a list of emails and names to show to the user # form a list of emails and names to show to the user
credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users_formatted:
if not credit_controller_users:
frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
message = """Please contact any of the following users to extend the credit limits for {0}: message = """Please contact any of the following users to extend the credit limits for {0}:
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users)) <br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users_formatted))
# if the current user does not have permissions to override credit limit, # if the current user does not have permissions to override credit limit,
# prompt them to send out an email to the controller users # prompt them to send out an email to the controller users
@ -427,7 +426,7 @@ def send_emails(args):
subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) subject = (_("Credit limit reached for customer {0}").format(args.get('customer')))
message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") message = (_("Credit limit has been crossed for customer {0} ({1}/{2})")
.format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit')))
frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message)
def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None):
# Outstanding based on GL Entries # Outstanding based on GL Entries

View File

@ -33,7 +33,7 @@ def get_data():
}, },
{ {
'label': _('Support'), 'label': _('Support'),
'items': ['Issue', 'Maintenance Visit'] 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim']
}, },
{ {
'label': _('Projects'), 'label': _('Projects'),

View File

@ -285,9 +285,17 @@ def _make_customer(source_name, ignore_permissions=False):
return customer return customer
else: else:
raise raise
except frappe.MandatoryError: except frappe.MandatoryError as e:
mandatory_fields = e.args[0].split(':')[1].split(',')
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
frappe.local.message_log = [] frappe.local.message_log = []
frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
message += _("Please create Customer from Lead {0}.").format(lead_link)
frappe.throw(message, title=_("Mandatory Missing"))
else: else:
return customer_name return customer_name
else: else:

View File

@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemCart = class {
onchange: function() { onchange: function() {
if (this.value || this.value == 0) { if (this.value || this.value == 0) {
const frm = me.events.get_frm(); const frm = me.events.get_frm();
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', this.value); frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
me.hide_discount_control(this.value); me.hide_discount_control(this.value);
} }
}, },

View File

@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company):
validate_filters(from_date, to_date, company) validate_filters(from_date, to_date, company)
active_leads = frappe.db.sql("""select count(*) from `tabLead` active_leads = frappe.db.sql("""select count(*) from `tabLead`
where (date(`modified`) between %s and %s) where (date(`creation`) between %s and %s)
and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] and company=%s""", (from_date, to_date, company))[0][0]
active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact
left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0]
opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` opportunities = frappe.db.sql("""select count(*) from `tabOpportunity`
where (date(`creation`) between %s and %s) where (date(`creation`) between %s and %s)
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0]
quotations = frappe.db.sql("""select count(*) from `tabQuotation` quotations = frappe.db.sql("""select count(*) from `tabQuotation`
where docstatus = 1 and (date(`creation`) between %s and %s) where docstatus = 1 and (date(`creation`) between %s and %s)
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0]
converted = frappe.db.sql("""select count(*) from `tabCustomer`
JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name
WHERE (date(`tabCustomer`.creation) between %s and %s)
and `tabLead`.company=%s""", (from_date, to_date, company))[0][0]
sales_orders = frappe.db.sql("""select count(*) from `tabSales Order`
where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0]
return [ return [
{ "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" }, { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" },
{ "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" },
{ "title": _("Quotations"), "value": quotations, "color": "#006685" }, { "title": _("Quotations"), "value": quotations, "color": "#006685" },
{ "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" } { "title": _("Converted"), "value": converted, "color": "#00AD65" }
] ]
@frappe.whitelist() @frappe.whitelist()

View File

@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype):
"label": _(partner_doctype), "label": _(partner_doctype),
"fieldtype": "Link", "fieldtype": "Link",
"options": partner_doctype, "options": partner_doctype,
"width": 100 "width": 150
}, { }, {
"fieldname": "item_group", "fieldname": "item_group",
"label": _("Item Group"), "label": _("Item Group"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Item Group", "options": "Item Group",
"width": 100 "width": 150
}] }]
for period in period_list: for period in period_list:
@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype):
"label": _("Target ({})").format(period.label), "label": _("Target ({})").format(period.label),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}, { }, {
"fieldname": period.key, "fieldname": period.key,
"label": _("Achieved ({})").format(period.label), "label": _("Achieved ({})").format(period.label),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}, { }, {
"fieldname": variance_key, "fieldname": variance_key,
"label": _("Variance ({})").format(period.label), "label": _("Variance ({})").format(period.label),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}]) }])
columns.extend([{ columns.extend([{
@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype):
"label": _("Total Target"), "label": _("Total Target"),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}, { }, {
"fieldname": "total_achieved", "fieldname": "total_achieved",
"label": _("Total Achieved"), "label": _("Total Achieved"),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}, { }, {
"fieldname": "total_variance", "fieldname": "total_variance",
"label": _("Total Variance"), "label": _("Total Variance"),
"fieldtype": fieldtype, "fieldtype": fieldtype,
"options": options, "options": options,
"width": 100 "width": 150
}]) }])
return columns return columns
@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list
if (r.get(sales_field) == d.parent and r.item_group == d.item_group and if (r.get(sales_field) == d.parent and r.item_group == d.item_group and
period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date):
details[p_key] += r.get(qty_or_amount_field, 0) details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(target_key) - details.get(p_key) details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key) details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_target") - details.get("total_achieved") details["total_variance"] = details.get("total_achieved") - details.get("total_target")
return rows return rows

View File

@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = {
options: "Quantity\nAmount", options: "Quantity\nAmount",
default: "Quantity" default: "Quantity"
}, },
] ],
"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;
}
} }

View File

@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = {
options: "Quantity\nAmount", options: "Quantity\nAmount",
default: "Quantity" default: "Quantity"
}, },
] ],
"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;
}
} }

View File

@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = {
options: "Quantity\nAmount", options: "Quantity\nAmount",
default: "Quantity" default: "Quantity"
}, },
] ],
"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;
}
} }

View File

@ -26,7 +26,8 @@ def delete_company_transactions(company_name):
tabDocField where fieldtype='Link' and options='Company'"""): tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", 'BOM'): "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@ -20,12 +20,14 @@ def after_install():
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert() frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
set_single_defaults() set_single_defaults()
create_compact_item_print_custom_field() create_compact_item_print_custom_field()
create_print_uom_after_qty_custom_field()
create_print_zero_amount_taxes_custom_field() create_print_zero_amount_taxes_custom_field()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")
create_default_cash_flow_mapper_templates() create_default_cash_flow_mapper_templates()
create_default_success_action() create_default_success_action()
create_default_energy_point_rules() create_default_energy_point_rules()
add_company_to_session_defaults() add_company_to_session_defaults()
add_standard_navbar_items()
frappe.db.commit() frappe.db.commit()
@ -65,6 +67,16 @@ def create_compact_item_print_custom_field():
}) })
def create_print_uom_after_qty_custom_field():
create_custom_field('Print Settings', {
'label': _('Print UOM after Quantity'),
'fieldname': 'print_uom_after_quantity',
'fieldtype': 'Check',
'default': 0,
'insert_after': 'compact_item_print'
})
def create_print_zero_amount_taxes_custom_field(): def create_print_zero_amount_taxes_custom_field():
create_custom_field('Print Settings', { create_custom_field('Print Settings', {
'label': _('Print taxes with zero amount'), 'label': _('Print taxes with zero amount'),
@ -104,3 +116,45 @@ def add_company_to_session_defaults():
"ref_doctype": "Company" "ref_doctype": "Company"
}) })
settings.save() settings.save()
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
erpnext_navbar_items = [
{
'item_label': 'Documentation',
'item_type': 'Route',
'route': 'https://erpnext.com/docs/user/manual',
'is_standard': 1
},
{
'item_label': 'User Forum',
'item_type': 'Route',
'route': 'https://discuss.erpnext.com',
'is_standard': 1
},
{
'item_label': 'Report an Issue',
'item_type': 'Route',
'route': 'https://github.com/frappe/erpnext/issues',
'is_standard': 1
}
]
current_nabvar_items = navbar_settings.help_dropdown
navbar_settings.set('help_dropdown', [])
for item in erpnext_navbar_items:
navbar_settings.append('help_dropdown', item)
for item in current_nabvar_items:
navbar_settings.append('help_dropdown', {
'item_label': item.item_label,
'item_type': item.item_type,
'route': item.route,
'action': item.action,
'is_standard': item.is_standard,
'hidden': item.hidden
})
navbar_settings.save()

View File

@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None):
if field == "outstanding_amount": if field == "outstanding_amount":
filters = [['docstatus', '=', '1'], ['company', '=', company]] filters = [['docstatus', '=', '1'], ['company', '=', company]]
if date_range: if date_range:
filters.append(['posting_date', 'between' [date_range[0], date_range[1]]]) date_range = frappe.parse_json(date_range)
filters.append(['posting_date', 'between', [date_range[0], date_range[1]]])
return frappe.db.get_all('Purchase Invoice', return frappe.db.get_all('Purchase Invoice',
fields = ['supplier as name', 'sum(outstanding_amount) as value'], fields = ['supplier as name', 'sum(outstanding_amount) as value'],
filters = filters, filters = filters,

View File

@ -33,7 +33,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Key Reports", "label": "Key Reports",
"links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -58,7 +58,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Stock", "label": "Stock",
"modified": "2020-05-30 17:32:11.062681", "modified": "2020-08-11 17:29:32.626067",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock", "name": "Stock",

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import json
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, cstr, today, random_string from frappe.utils import cint, flt, cstr, today, random_string
@ -159,6 +160,71 @@ class TestPurchaseReceipt(unittest.TestCase):
set_perpetual_inventory(0) set_perpetual_inventory(0)
def test_subcontracting_over_receipt(self):
"""
Behaviour: Raise multiple PRs against one PO that in total
receive more than the required qty in the PO.
Expected Result: Error Raised for Over Receipt against PO.
"""
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on,
make_subcontracted_item, create_purchase_order)
from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt,
make_rm_stock_entry as make_subcontract_transfer_entry)
update_backflush_based_on("Material Transferred for Subcontract")
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer
make_stock_entry(target="_Test Warehouse - _TC",
item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=1, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test Item", qty=1, basic_rate=100)
rm_items = [
{
"item_code": item_code,
"rm_item_code": po.supplied_items[0].rm_item_code,
"item_name": "_Test Item",
"qty": po.supplied_items[0].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
},
{
"item_code": item_code,
"rm_item_code": po.supplied_items[1].rm_item_code,
"item_name": "Test Extra Item 1",
"qty": po.supplied_items[1].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
},
{
"item_code": item_code,
"rm_item_code": po.supplied_items[2].rm_item_code,
"item_name": "_Test Item Home Desktop 100",
"qty": po.supplied_items[2].required_qty,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos"
}
]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.to_warehouse = "_Test Warehouse 1 - _TC"
se.save()
se.submit()
pr1 = make_purchase_receipt(po.name)
pr2 = make_purchase_receipt(po.name)
pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit)
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),

View File

@ -513,7 +513,7 @@ class StockEntry(StockController):
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
d.basic_amount = d.basic_rate * d.qty d.basic_amount = d.basic_rate * flt(d.qty)
def distribute_additional_costs(self): def distribute_additional_costs(self):
if self.purpose == "Material Issue": if self.purpose == "Material Issue":

View File

@ -258,6 +258,7 @@ class StockReconciliation(StockController):
sl_entries.append(args) sl_entries.append(args)
qty_after_transaction = 0
for serial_no in serial_nos: for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no]) args = self.get_sle_for_items(row, [serial_no])
@ -271,11 +272,19 @@ class StockReconciliation(StockController):
if previous_sle and row.warehouse != previous_sle.get("warehouse"): if previous_sle and row.warehouse != previous_sle.get("warehouse"):
# If serial no exists in different warehouse # If serial no exists in different warehouse
warehouse = previous_sle.get("warehouse", '') or row.warehouse
if not qty_after_transaction:
qty_after_transaction = get_stock_balance(row.item_code,
warehouse, self.posting_date, self.posting_time)
qty_after_transaction -= 1
new_args = args.copy() new_args = args.copy()
new_args.update({ new_args.update({
'actual_qty': -1, 'actual_qty': -1,
'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1, 'qty_after_transaction': qty_after_transaction,
'warehouse': previous_sle.get("warehouse", '') or row.warehouse, 'warehouse': warehouse,
'valuation_rate': previous_sle.get("valuation_rate") 'valuation_rate': previous_sle.get("valuation_rate")
}) })

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