Merge branch 'develop' into putaway

This commit is contained in:
Marica 2020-12-08 11:44:00 +05:30 committed by GitHub
commit 0b68243979
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 6595 additions and 1564 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# Root editor config file
root = true
# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js}]
indent_style = tab
indent_size = 4

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help.

View File

@ -9,5 +9,6 @@
"root_login": "root",
"root_password": "travis",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"]
"install_apps": ["erpnext"],
"throttle_user_limit": 100
}

View File

@ -6,9 +6,8 @@ import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
from frappe.utils.dashboard import cache_source
from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()

View File

@ -43,7 +43,7 @@
{
"hidden": 0,
"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 Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\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 Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,

View File

@ -245,6 +245,9 @@ def get():
"account_number": "2200"
},
_("Duties and Taxes"): {
_("TDS Payable"): {
"account_number": "2310"
},
"account_type": "Tax",
"is_group": 1,
"account_number": "2300"

View File

@ -9,11 +9,13 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
def setUp(self):
make_pos_profile()
add_transactions()
add_payments()
@ -27,6 +29,9 @@ class TestBankTransaction(unittest.TestCase):
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False

View File

@ -94,8 +94,7 @@ frappe.ui.form.on('Chart of Accounts Importer', {
callback: function(r) {
if(r.message===false) {
frm.set_value("company", "");
frappe.throw(__(`Transactions against the company already exist!
Chart Of accounts can be imported for company with no transactions`));
frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."));
} else {
frm.trigger("refresh");
}

View File

@ -137,11 +137,12 @@ class InvoiceDiscounting(AccountsController):
"cost_center": erpnext.get_default_cost_center(self.company)
})
je.append("accounts", {
"account": self.bank_charges_account,
"debit_in_account_currency": flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company)
})
if self.bank_charges:
je.append("accounts", {
"account": self.bank_charges_account,
"debit_in_account_currency": flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company)
})
je.append("accounts", {
"account": self.short_term_loan,

View File

@ -80,6 +80,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
bank_charges=100
)
je = inv_disc.create_disbursement_entry()
@ -289,6 +290,7 @@ def create_invoice_discounting(invoices, **args):
inv_disc.bank_account=args.bank_account
inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30
inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices:
inv_disc.append("invoices", {

View File

@ -34,6 +34,7 @@ class JournalEntry(AccountsController):
self.validate_entries_for_advance()
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
@ -339,8 +340,7 @@ class JournalEntry(AccountsController):
currency=account_currency)
if flt(voucher_total) < (flt(order.advance_paid) + total):
frappe.throw(_("Advance paid against {0} {1} cannot be greater \
than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
@ -369,6 +369,11 @@ class JournalEntry(AccountsController):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get('accounts'):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:

View File

@ -1,13 +1,17 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return{
filters: [
['Account', 'account_type', 'in', 'Bank, Cash, Receivable'],
['Account', 'is_group', '=', 0],
['Account', 'company', '=', d.company]
]
}
});
frappe.ui.form.on('Mode of Payment', {
setup: function(frm) {
frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: [
['Account', 'account_type', 'in', 'Bank, Cash, Receivable'],
['Account', 'is_group', '=', 0],
['Account', 'company', '=', d.company]
]
};
});
},
});

View File

@ -155,7 +155,8 @@ class OpeningInvoiceCreationTool(Document):
"posting_date": row.posting_date,
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
})
accounting_dimension = get_accounting_dimensions()

View File

@ -7,17 +7,24 @@ import frappe
import unittest
test_dependencies = ["Customer", "Supplier"]
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def make_invoices(self, invoice_type="Sales"):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type)
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
invoices = self.make_invoices()
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@ -27,6 +34,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
}
self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
property_setter.delete()
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@ -36,7 +50,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase")
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@ -46,6 +60,32 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
}
self.check_expected_values(invoices, expected_value, "Purchase")
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
"is_group": 1, "company": "_Test Opening Invoice Company"})
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
self.assertTrue(error_log)
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@ -57,7 +97,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 1.0,
"outstanding_amount": 300,
"party": "_Test {0}".format(party),
"party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@ -66,7 +106,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 2.0,
"outstanding_amount": 250,
"party": "_Test {0} 1".format(party),
"party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@ -76,4 +116,31 @@ def get_opening_invoice_creation_dict(**args):
})
invoice_dict.update(args)
return invoice_dict
return invoice_dict
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
company = frappe.new_doc("Company")
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.insert()
return company
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_type": "Company",
"territory": "All Territories"
})
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
else:
return frappe.db.exists("Customer", customer_name)

View File

@ -202,17 +202,32 @@ class PaymentEntry(AccountsController):
# if account_type not in account_types:
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self):
def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc)
def set_source_exchange_rate(self, ref_doc=None):
if self.paid_from and not self.source_exchange_rate:
if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1
else:
self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
self.company_currency, self.posting_date)
if ref_doc:
if self.paid_from_account_currency == ref_doc.currency:
self.source_exchange_rate = ref_doc.get("exchange_rate")
if not self.source_exchange_rate:
self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
self.company_currency, self.posting_date)
def set_target_exchange_rate(self, ref_doc=None):
if self.paid_to and not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
self.company_currency, self.posting_date)
if ref_doc:
if self.paid_to_account_currency == ref_doc.currency:
self.target_exchange_rate = ref_doc.get("exchange_rate")
if not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
self.company_currency, self.posting_date)
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
@ -282,9 +297,10 @@ class PaymentEntry(AccountsController):
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
for k, v in no_oustanding_refs.items():
frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.<br><br>\
If this is undesirable please cancel the corresponding Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")),
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@ -909,22 +925,24 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
elif reference_doctype != "Journal Entry":
if party_account_currency == company_currency:
if ref_doc.doctype == "Expense Claim":
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
else:
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
# or get it based on the posting date of the ref doc.
exchange_rate = ref_doc.get("conversion_rate") or \
get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
@ -932,11 +950,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
elif reference_doctype == "Employee Advance":
outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
# Get the exchange rate based on the posting date of the ref doc
# Get the exchange rate based on the posting date of the ref doc.
exchange_rate = get_exchange_rate(party_account_currency,
company_currency, ref_doc.posting_date)
@ -948,102 +970,104 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
"bill_no": bill_no
})
def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name):
total_amount, outstanding_amount, exchange_rate = None
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
outstanding_amount = ref_doc.get("dunning_amount")
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
return total_amount, outstanding_amount, exchange_rate
def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency):
total_amount, outstanding_amount, exchange_rate = None
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc)
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
exchange_rate = ref_doc.get("conversion_rate") or \
get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts(
reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency)
return total_amount, outstanding_amount, exchange_rate, bill_no
def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc):
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
return total_amount, exchange_rate
def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc):
exchange_rate = None
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
return total_amount, exchange_rate
def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency):
outstanding_amount, bill_no = None
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
elif reference_doctype == "Employee Advance":
outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
return outstanding_amount, exchange_rate, bill_no
@frappe.whitelist()
def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee"
elif dt in ("Fees"):
party_type = "Student"
# party account
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
elif dt == "Purchase Invoice":
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
elif dt == "Employee Advance":
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
else:
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
# payment type
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
payment_type = "Pay"
# amounts
grand_total = outstanding_amount = 0
if party_amount:
grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"):
if party_account_currency == doc.company_currency:
grand_total = doc.base_rounded_total or doc.base_grand_total
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = doc.advance_amount
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
else:
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
party_type = set_party_type(dt)
party_account = set_party_account(dt, dn, doc, party_type)
party_account_currency = set_party_account_currency(dt, party_account, doc)
payment_type = set_payment_type(dt, doc)
grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc)
# bank or cash
bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
bank = get_bank_cash_account(doc, bank_account)
if not bank:
bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get('conversion_rate', 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get('conversion_rate', 1)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
@ -1115,10 +1139,120 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.setup_party_account_field()
pe.set_missing_values()
if party_account and bank:
pe.set_exchange_rate()
if dt == "Employee Advance":
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
return pe
def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
if not bank:
bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
return bank
def set_party_type(dt):
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee"
elif dt in ("Fees"):
party_type = "Student"
return party_type
def set_party_account(dt, dn, doc, party_type):
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
elif dt == "Purchase Invoice":
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
elif dt == "Employee Advance":
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
def set_party_account_currency(dt, party_account, doc):
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
else:
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
return party_account_currency
def set_payment_type(dt, doc):
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
payment_type = "Pay"
return payment_type
def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc):
grand_total = outstanding_amount = 0
if party_amount:
grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"):
if party_account_currency == doc.company_currency:
grand_total = doc.base_rounded_total or doc.base_grand_total
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = flt(doc.advance_amount)
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
if party_account_currency != doc.currency:
grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate)
outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate)
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
else:
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
return grand_total, outstanding_amount
def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc):
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
received_amount = paid_amount * doc.get('exchange_rate', 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = []
for payment_term in payment_schedule:

View File

@ -35,6 +35,15 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query("taxes_and_charges", function() {
return {
filters: [
['Sales Taxes and Charges Template', 'company', '=', frm.doc.company],
['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
]
};
});
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));

View File

@ -70,6 +70,7 @@ def get_items_list(pos_profile, company):
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Payment Method`")
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)

View File

@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
${__('Notes')}
{{__('Notes')}}
</h4>
<ul>
<li>
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
{{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
</li>
<li>
${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}
{{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
</li>
<li>
${__('Discount Percentage can be applied either against a Price List or for all Price List.')}
{{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
</li>
<li>
${__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}
{{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
</li>
</ul>
</td></tr>
<tr><td>
<h4><i class="fa fa-question-sign"></i>
${__('How Pricing Rule is applied?')}
{{__('How Pricing Rule is applied?')}}
</h4>
<ol>
<li>
${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}
{{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
</li>
<li>
${__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}
{{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
</li>
<li>
${__('Pricing Rules are further filtered based on quantity.')}
{{__('Pricing Rules are further filtered based on quantity.')}}
</li>
<li>
${__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}
{{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
</li>
<li>
${__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}
{{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
<ul>
<li>
${__('Item Code > Item Group > Brand')}
{{__('Item Code > Item Group > Brand')}}
</li>
<li>
${__('Customer > Customer Group > Territory')}
{{__('Customer > Customer Group > Territory')}}
</li>
<li>
${__('Supplier > Supplier Type')}
{{__('Supplier > Supplier Type')}}
</li>
</ul>
</li>
<li>
${__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}
{{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
</li>
</ol>
</td></tr>

View File

@ -406,6 +406,7 @@
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
@ -469,6 +470,7 @@
"options": "UOM"
},
{
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Rate"
@ -563,7 +565,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2020-10-28 16:53:14.416172",
"modified": "2020-12-04 00:36:24.698219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -521,6 +521,22 @@ class TestPricingRule(unittest.TestCase):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
si = create_sales_invoice(qty=5, do_not_submit=True)
self.assertEquals(len(si.items), 2)
self.assertEquals(si.items[1].rate, 10)
si1 = create_sales_invoice(qty=2, do_not_submit=True)
self.assertEquals(len(si1.items), 1)
for doc in [si, si1]:
doc.delete()
def make_pricing_rule(**args):
args = frappe._dict(args)
@ -539,20 +555,23 @@ def make_pricing_rule(**args):
"rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0,
"margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
if args.get("priority"):
doc.priority = args.get("priority")
for field in ["free_item", "free_qty", "free_item_rate", "priority",
"margin_type", "price_or_product_discount"]:
if args.get(field):
doc.set(field, args.get(field))
apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item"
})
if doc.apply_on != "Transaction":
doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item"
})
doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code":

View File

@ -457,6 +457,9 @@ def apply_pricing_rule_on_transaction(doc):
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
doc.total, pricing_rules)
if not pricing_rules:
remove_free_item(doc)
for d in pricing_rules:
if d.price_or_product_discount == 'Price':
if d.apply_discount_on:
@ -480,6 +483,12 @@ def apply_pricing_rule_on_transaction(doc):
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
doc.calculate_taxes_and_totals()
def remove_free_item(doc):
for d in doc.items:
if d.is_free_item:
doc.remove(d)
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
@ -492,7 +501,7 @@ def get_applied_pricing_rules(pricing_rules):
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
if pricing_rule.same_item:
if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
free_item = item_details.item_code or args.item_code
if not free_item:

View File

@ -21,7 +21,7 @@ class TestProcessDeferredAccounting(unittest.TestCase):
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"

View File

@ -147,6 +147,11 @@ class PurchaseInvoice(BuyingController):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
if not self.credit_to:
self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
account = frappe.db.get_value("Account", self.credit_to,
["account_type", "report_type", "account_currency"], as_dict=True)
@ -1032,7 +1037,9 @@ class PurchaseInvoice(BuyingController):
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified)
for pr in set(updated_pr):
frappe.get_doc("Purchase Receipt", pr).update_billing_percentage(update_modified=update_modified)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None

View File

@ -1,92 +1,38 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-07-27 17:24:24.956896",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2016-07-27 17:24:24.956896",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"account"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.",
"fieldname": "default_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Default Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.",
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-09-02 07:49:06.567389",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2020-10-18 17:57:57.110257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -405,6 +405,8 @@ class SalesInvoice(SellingController):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name')
pos = {}
@ -472,6 +474,11 @@ class SalesInvoice(SellingController):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
def validate_debit_to_acc(self):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
if not self.debit_to:
self.raise_missing_debit_credit_account_error("Customer", self.customer)
account = frappe.get_cached_value("Account", self.debit_to,
["account_type", "report_type", "account_currency"], as_dict=True)

View File

@ -690,7 +690,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle)
def test_pos_gl_entry_with_perpetual_inventory(self):
make_pos_profile()
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@ -746,7 +747,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos_return.get('payments')[0].amount, -1000)
def test_pos_change_amount(self):
make_pos_profile()
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")

View File

@ -156,7 +156,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
setup_transactions_dom() {
const me = this;
me.parent.$main_section.append(`<div class="transactions-table"></div>`)
me.parent.$main_section.append('<div class="transactions-table"></div>');
}
create_datatable() {
@ -167,9 +167,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
})
}
catch(err) {
let msg = __(`Your file could not be processed by ERPNext.
<br>It should be a standard CSV or XLSX file.
<br>The headers should be in the first row.`)
let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
frappe.throw(msg)
}

View File

@ -14,11 +14,93 @@ def execute(filters=None):
def get_column():
return [
_("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100",
_("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120",
_("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120",
_("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100",
_("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120",
{
"label": _("Delivery Note"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Delivery Note",
"width": 160
},
{
"label": _("Date"),
"fieldname": "date",
"fieldtype": "Date",
"width": 100
},
{
"label": _("Customer"),
"fieldname": "customer",
"fieldtype": "Link",
"options": "Customer",
"width": 120
},
{
"label": _("Customer Name"),
"fieldname": "customer_name",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 120
},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"width": 100,
"options": "Company:company:default_currency"
},
{
"label": _("Billed Amount"),
"fieldname": "billed_amount",
"fieldtype": "Currency",
"width": 100,
"options": "Company:company:default_currency"
},
{
"label": _("Returned Amount"),
"fieldname": "returned_amount",
"fieldtype": "Currency",
"width": 120,
"options": "Company:company:default_currency"
},
{
"label": _("Pending Amount"),
"fieldname": "pending_amount",
"fieldtype": "Currency",
"width": 120,
"options": "Company:company:default_currency"
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Description"),
"fieldname": "description",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"width": 120
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120
}
]
def get_args():

View File

@ -17,18 +17,26 @@ def get_ordered_to_be_billed_data(args):
return frappe.db.sql("""
Select
`{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
{project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount,
`{parent_tab}`.name, `{parent_tab}`.{date_field},
`{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
`{child_tab}`.item_code,
`{child_tab}`.base_amount,
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)),
(`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))),
`{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)),
(`{child_tab}`.base_amount -
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) -
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))),
`{child_tab}`.item_name, `{child_tab}`.description,
{project_field}, `{parent_tab}`.company
from
`{parent_tab}`, `{child_tab}`
where
`{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1
and `{parent_tab}`.status not in ('Closed', 'Completed')
and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt *
ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount
and `{child_tab}`.amount > 0
and (`{child_tab}`.base_amount -
round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) -
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0
order by
`{parent_tab}`.{order} {order_by}
""".format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party,

View File

@ -14,11 +14,93 @@ def execute(filters=None):
def get_column():
return [
_("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100",
_("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120",
_("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120",
_("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100",
_("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120",
{
"label": _("Purchase Receipt"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Purchase Receipt",
"width": 160
},
{
"label": _("Date"),
"fieldname": "date",
"fieldtype": "Date",
"width": 100
},
{
"label": _("Supplier"),
"fieldname": "supplier",
"fieldtype": "Link",
"options": "Supplier",
"width": 120
},
{
"label": _("Supplier Name"),
"fieldname": "supplier_name",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 120
},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"width": 100,
"options": "Company:company:default_currency"
},
{
"label": _("Billed Amount"),
"fieldname": "billed_amount",
"fieldtype": "Currency",
"width": 100,
"options": "Company:company:default_currency"
},
{
"label": _("Returned Amount"),
"fieldname": "returned_amount",
"fieldtype": "Currency",
"width": 120,
"options": "Company:company:default_currency"
},
{
"label": _("Pending Amount"),
"fieldname": "pending_amount",
"fieldtype": "Currency",
"width": 120,
"options": "Company:company:default_currency"
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Description"),
"fieldname": "description",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"width": 120
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120
}
]
def get_args():

View File

@ -78,7 +78,10 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
error_msg = _("""{0} {1} not in any active Fiscal Year.""").format(label, formatdate(transaction_date))
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)

View File

@ -373,8 +373,8 @@ frappe.ui.form.on('Asset', {
doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, '');
frappe.msgprint({
title: __(`Invalid ${doctype}`),
message: __(`The selected ${doctype} doesn't contains selected Asset Item.`),
title: __('Invalid {0}', [__(doctype)]),
message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]),
indicator: 'red'
});
}
@ -436,7 +436,7 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn];
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`));
frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date."));
book.depreciation_start_date = "";
frm.refresh_field("finance_books");
}

View File

@ -168,6 +168,7 @@
"bold": 1,
"fieldname": "supplier",
"fieldtype": "Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Supplier",
"oldfieldname": "supplier",
@ -1106,7 +1107,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2020-10-30 13:58:14.697921",
"modified": "2020-12-03 16:46:44.229351",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -290,11 +290,17 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
dialog.show();
}, __("Get Items From"));
// Link Material Requests
this.frm.add_custom_button(__('Link to Material Requests'),
function() {
erpnext.buying.link_to_mrs(me.frm);
}, __("Tools"));
// Get Suppliers
this.frm.add_custom_button(__('Get Suppliers'),
function() {
me.get_suppliers_button(me.frm);
});
}, __("Tools"));
}
},

View File

@ -18,7 +18,6 @@
"suppliers",
"items_section",
"items",
"link_to_mrs",
"supplier_response_section",
"salutation",
"subject",
@ -118,13 +117,6 @@
"reqd": 1
},
{
"depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)",
"fieldname": "link_to_mrs",
"fieldtype": "Button",
"label": "Link to Material Requests"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "supplier_response_section",
"fieldtype": "Section Break",
"label": "Email Details"
@ -260,7 +252,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-04 22:04:29.017134",
"modified": "2020-11-05 22:04:29.017134",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@ -50,6 +50,12 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
})
}, __("Get Items From"));
// Link Material Requests
this.frm.add_custom_button(__('Link to Material Requests'),
function() {
erpnext.buying.link_to_mrs(me.frm);
}, __("Tools"));
this.frm.add_custom_button(__("Request for Quotation"),
function() {
if (!me.frm.doc.supplier) {

View File

@ -35,7 +35,6 @@
"ignore_pricing_rule",
"items_section",
"items",
"link_to_mrs",
"pricing_rule_details",
"pricing_rules",
"section_break_22",
@ -322,12 +321,6 @@
"options": "Supplier Quotation Item",
"reqd": 1
},
{
"depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)",
"fieldname": "link_to_mrs",
"fieldtype": "Button",
"label": "Link to material requests"
},
{
"fieldname": "pricing_rule_details",
"fieldtype": "Section Break",
@ -806,9 +799,10 @@
],
"icon": "fa fa-shopping-cart",
"idx": 29,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-30 13:58:33.043971",
"modified": "2020-12-03 15:18:29.073368",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
class AccountMissingError(frappe.ValidationError): pass
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
class AccountsController(TransactionBase):
@ -735,6 +737,21 @@ class AccountsController(TransactionBase):
return self._abbr
def raise_missing_debit_credit_account_error(self, party_type, party):
"""Raise an error if debit to/credit to account does not exist."""
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
link_to_party = frappe.utils.get_link_to_form(party_type, party)
link_to_company = frappe.utils.get_link_to_form("Company", self.company)
message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '')
message += "<br>" + _("Please set one of the following:") + "<br>"
message += "<br><ul><li>" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "</li>"
message += "<li>" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "</li></ul>"
frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)
def validate_party(self):
party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party)

View File

@ -497,6 +497,10 @@ class BuyingController(StockController):
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
if self.doctype=="Purchase Receipt" and d.meta.get_field("received_stock_qty"):
# Set Received Qty in Stock UOM
d.received_stock_qty = flt(d.received_qty) * flt(d.conversion_factor, d.precision("conversion_factor"))
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:

View File

@ -203,10 +203,37 @@ def get_already_returned_items(doc):
return items
def get_returned_qty_map_for_row(row_name, doctype):
child_doctype = doctype + " Item"
reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail"
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
]
if doctype == "Purchase Receipt":
fields += [
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)
]
data = frappe.db.get_list(doctype,
fields = fields,
filters = [
[doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name]
])
return data[0]
def make_return_doc(doctype, source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return")
def set_missing_values(source, target):
doc = frappe.get_doc(target)
doc.is_return = 1
@ -261,20 +288,25 @@ def make_return_doc(doctype, source_name, target_doc=None):
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1* source_doc.qty
target_doc.qty = -1 * source_doc.qty
if doctype == "Purchase Receipt":
target_doc.received_qty = -1* source_doc.received_qty
target_doc.rejected_qty = -1* source_doc.rejected_qty
target_doc.qty = -1* source_doc.qty
target_doc.stock_qty = -1 * source_doc.stock_qty
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
target_doc.received_stock_qty = -1 * flt(source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0))
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_order_item = source_doc.purchase_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
target_doc.received_qty = -1* source_doc.received_qty
target_doc.rejected_qty = -1* source_doc.rejected_qty
target_doc.received_qty = -1 * source_doc.received_qty
target_doc.rejected_qty = -1 * source_doc.rejected_qty
target_doc.qty = -1* source_doc.qty
target_doc.stock_qty = -1 * source_doc.stock_qty
target_doc.purchase_order = source_doc.purchase_order
@ -285,6 +317,10 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice
target_doc.so_detail = source_doc.so_detail

View File

@ -42,7 +42,7 @@ class SellingController(StockController):
self.validate_max_discount()
self.validate_selling_price()
self.set_qty_as_per_stock_uom()
self.set_po_nos()
self.set_po_nos(for_validate=True)
self.set_gross_profit()
set_default_income_account_for_item(self)
self.set_customer_address()
@ -370,20 +370,28 @@ class SellingController(StockController):
}))
self.make_sl_entries(sl_entries)
def set_po_nos(self):
def set_po_nos(self, for_validate=False):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
if for_validate and self.po_no:
return
self.set_pos_for_sales_invoice()
if self.doctype == 'Delivery Note' and hasattr(self, "items"):
if for_validate and self.po_no:
return
self.set_pos_for_delivery_note()
def set_pos_for_sales_invoice(self):
po_nos = []
if self.po_no:
po_nos.append(self.po_no)
self.get_po_nos('Sales Order', 'sales_order', po_nos)
self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def set_pos_for_delivery_note(self):
po_nos = []
if self.po_no:
po_nos.append(self.po_no)
self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))

View File

@ -58,6 +58,7 @@ status_map = {
"Delivery Note": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
@ -65,6 +66,7 @@ status_map = {
"Purchase Receipt": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
@ -232,7 +234,7 @@ class StatusUpdater(Document):
self._update_children(args, update_modified)
if "percent_join_field" in args:
if "percent_join_field" in args or "percent_join_field_parent" in args:
self._update_percent_field_in_targets(args, update_modified)
def _update_children(self, args, update_modified):
@ -272,13 +274,19 @@ class StatusUpdater(Document):
def _update_percent_field_in_targets(self, args, update_modified=True):
"""Update percent field in parent transaction"""
distinct_transactions = set([d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt'])])
if args.get('percent_join_field_parent'):
# if reference to target doc where % is to be updated, is
# in source doc's parent form, consider percent_join_field_parent
args['name'] = self.get(args['percent_join_field_parent'])
self._update_percent_field(args, update_modified)
else:
distinct_transactions = set([d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt'])])
for name in distinct_transactions:
if name:
args['name'] = name
self._update_percent_field(args, update_modified)
for name in distinct_transactions:
if name:
args['name'] = name
self._update_percent_field(args, update_modified)
def _update_percent_field(self, args, update_modified=True):
"""Update percent field in parent transaction"""

View File

@ -342,11 +342,15 @@ class StockController(AccountsController):
validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True):
target_ref_field = "amount"
if self.doctype == "Delivery Note":
target_ref_field = "amount - (returned_qty * rate)"
self._update_percent_field({
"target_dt": self.doctype + " Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_billed",
"target_ref_field": "amount",
"target_ref_field": target_ref_field,
"target_field": "billed_amt",
"name": self.name,
}, update_modified)

View File

@ -641,7 +641,8 @@ class calculate_taxes_and_totals(object):
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'amount': total_amount_to_pay
'amount': total_amount_to_pay,
'default': 1
})
else:
self.doc.is_pos = 0

View File

@ -4,7 +4,7 @@ function check_times(frm) {
let from_time = Date.parse('01/01/2019 ' + d.from_time);
let to_time = Date.parse('01/01/2019 ' + d.to_time);
if (from_time > to_time) {
frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`));
frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1]));
}
});
}

View File

@ -134,7 +134,7 @@ def setup_employee():
salary_component = frappe.get_doc('Salary Component', d.name)
salary_component.append('accounts', dict(
company=erpnext.get_default_company(),
default_account=frappe.get_value('Account', dict(account_name=('like', 'Salary%')))
account=frappe.get_value('Account', dict(account_name=('like', 'Salary%')))
))
salary_component.save()

View File

@ -1,5 +1,7 @@
import traceback
import taxjar
import frappe
from erpnext import get_default_company
from frappe import _
@ -29,7 +31,6 @@ def get_client():
def create_transaction(doc, method):
import taxjar
"""Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS:

View File

@ -60,4 +60,12 @@ def create_mode_of_payment(gateway, payment_type="General"):
"default_account": payment_gateway_account
}]
})
mode_of_payment.insert(ignore_permissions=True)
mode_of_payment.insert(ignore_permissions=True)
def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL.
tracking_url = ''
url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference')
if url_reference:
tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number})
return tracking_url

View File

@ -43,7 +43,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]"
"links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Inpatient Medication Orders\",\n\t\t\"doctype\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Orders\"\n\t}\n]"
}
],
"category": "Domains",
@ -64,7 +64,7 @@
"idx": 0,
"is_standard": 1,
"label": "Healthcare",
"modified": "2020-06-25 23:50:56.951698",
"modified": "2020-11-23 23:00:48.764377",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",

View File

@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (r.message) {
frappe.show_alert({
message: __('Stock Entry {0} created',
['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
message: __('Stock Entry {0} created', ['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
indicator: 'green'
});
}
@ -105,8 +104,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (!r.exc) {
if (r.message == 'insufficient stock') {
let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?',
[frm.doc.warehouse.bold()]);
let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]);
frappe.confirm(
msg,
function() {

View File

@ -7,6 +7,7 @@ import frappe
import unittest
from frappe.utils import nowdate, add_days
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_appointment, create_healthcare_service_items
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Company"]
@ -15,6 +16,7 @@ class TestFeeValidity(unittest.TestCase):
frappe.db.sql("""delete from `tabPatient Appointment`""")
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient`""")
make_pos_profile()
def test_fee_validity(self):
item = create_healthcare_service_items()

View File

@ -274,4 +274,6 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
return ip_record.inpatient_occupancies[-1].service_unit
if ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
return

View File

@ -7,12 +7,14 @@ import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
from frappe.utils import nowdate, add_days
from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientAppointment(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabPatient Appointment`""")
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""")
make_pos_profile()
def test_status(self):
patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -6,11 +6,13 @@ import unittest
import frappe
from frappe.utils import nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase):
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
make_pos_profile()
def test_medical_record(self):
patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -0,0 +1,57 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Inpatient Medication Orders"] = {
"filters": [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.now_date(),
reqd: 1
},
{
fieldname: "patient",
label: __("Patient"),
fieldtype: "Link",
options: "Patient"
},
{
fieldname: "service_unit",
label: __("Healthcare Service Unit"),
fieldtype: "Link",
options: "Healthcare Service Unit",
get_query: () => {
var company = frappe.query_report.get_filter_value('company');
return {
filters: {
'company': company,
'is_group': 0
}
}
}
},
{
fieldname: "show_completed_orders",
label: __("Show Completed Orders"),
fieldtype: "Check",
default: 1
}
]
};

View File

@ -0,0 +1,36 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2020-11-23 17:25:58.802949",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2020-11-23 19:40:20.227591",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Orders",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Inpatient Medication Order",
"report_name": "Inpatient Medication Orders",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Healthcare Administrator"
},
{
"role": "Nursing User"
},
{
"role": "Physician"
}
]
}

View File

@ -0,0 +1,198 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
chart = get_chart_data(data)
return columns, data, None, chart
def get_columns():
return [
{
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient",
"width": 200
},
{
"fieldname": "healthcare_service_unit",
"fieldtype": "Link",
"label": "Healthcare Service Unit",
"options": "Healthcare Service Unit",
"width": 150
},
{
"fieldname": "drug",
"fieldtype": "Link",
"label": "Drug Code",
"options": "Item",
"width": 150
},
{
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"width": 150
},
{
"fieldname": "dosage",
"fieldtype": "Link",
"label": "Dosage",
"options": "Prescription Dosage",
"width": 80
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"label": "Dosage Form",
"options": "Dosage Form",
"width": 100
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"width": 100
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"width": 100
},
{
"fieldname": "is_completed",
"fieldtype": "Check",
"label": "Is Order Completed",
"width": 100
},
{
"fieldname": "healthcare_practitioner",
"fieldtype": "Link",
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
"width": 200
},
{
"fieldname": "inpatient_medication_entry",
"fieldtype": "Link",
"label": "Inpatient Medication Entry",
"options": "Inpatient Medication Entry",
"width": 200
},
{
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"options": "Inpatient Record",
"width": 200
}
]
def get_data(filters):
conditions, values = get_conditions(filters)
data = frappe.db.sql("""
SELECT
parent.patient, parent.inpatient_record, parent.practitioner,
child.drug, child.drug_name, child.dosage, child.dosage_form,
child.date, child.time, child.is_completed, child.name
FROM `tabInpatient Medication Order` parent
INNER JOIN `tabInpatient Medication Order Entry` child
ON child.parent = parent.name
WHERE
parent.docstatus = 1
{conditions}
ORDER BY date, time
""".format(conditions=conditions), values, as_dict=1)
data = get_inpatient_details(data, filters.get("service_unit"))
return data
def get_conditions(filters):
conditions = ""
values = dict()
if filters.get("company"):
conditions += " AND parent.company = %(company)s"
values["company"] = filters.get("company")
if filters.get("from_date") and filters.get("to_date"):
conditions += " AND child.date BETWEEN %(from_date)s and %(to_date)s"
values["from_date"] = filters.get("from_date")
values["to_date"] = filters.get("to_date")
if filters.get("patient"):
conditions += " AND parent.patient = %(patient)s"
values["patient"] = filters.get("patient")
if not filters.get("show_completed_orders"):
conditions += " AND child.is_completed = 0"
return conditions, values
def get_inpatient_details(data, service_unit):
service_unit_filtered_data = []
for entry in data:
entry["healthcare_service_unit"] = get_current_healthcare_service_unit(entry.inpatient_record)
if entry.is_completed:
entry["inpatient_medication_entry"] = get_inpatient_medication_entry(entry.name)
if service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit:
service_unit_filtered_data.append(entry)
entry.pop("name", None)
for entry in service_unit_filtered_data:
data.remove(entry)
return data
def get_inpatient_medication_entry(order_entry):
return frappe.db.get_value("Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent")
def get_chart_data(data):
if not data:
return None
labels = ["Pending", "Completed"]
datasets = []
status_wise_data = {
"Pending": 0,
"Completed": 0
}
for d in data:
if d.is_completed:
status_wise_data["Completed"] += 1
else:
status_wise_data["Pending"] += 1
datasets.append({
"name": "Inpatient Medication Order Status",
"values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")]
})
chart = {
"data": {
"labels": labels,
"datasets": datasets
},
"type": "donut",
"height": 300
}
chart["fieldtype"] = "Data"
return chart

View File

@ -0,0 +1,128 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
import datetime
from frappe.utils import getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
from erpnext.healthcare.report.inpatient_medication_orders.inpatient_medication_orders import execute
class TestInpatientMedicationOrders(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.sql("delete from `tabInpatient Medication Order` where company='_Test Company'")
frappe.db.sql("delete from `tabInpatient Medication Entry` where company='_Test Company'")
self.patient = create_patient()
self.ip_record = create_records(self.patient)
def test_inpatient_medication_orders_report(self):
filters = {
'company': '_Test Company',
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC'
}
report = execute(filters)
expected_data = [
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=32400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=50400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=75600),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
}
]
self.assertEqual(expected_data, report[1])
filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
filters = {
'company': '_Test Company',
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC',
'show_completed_orders': 0
}
report = execute(filters)
self.assertEqual(len(report[1]), 0)
def tearDown(self):
if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def create_records(patient):
frappe.db.sql("""delete from `tabInpatient Record`""")
# Admit
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient)
ipmo.submit()
return ip_record

View File

@ -271,11 +271,11 @@ doc_events = {
},
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information",
"validate": "erpnext.crm.utils.update_lead_phone_numbers"
},
"Lead": {
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
@ -347,14 +347,16 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
@ -439,42 +441,43 @@ global_search_doctypes = {
{"doctype": "Sales Order", "index": 8},
{"doctype": "Quotation", "index": 9},
{"doctype": "Work Order", "index": 10},
{"doctype": "Purchase Receipt", "index": 11},
{"doctype": "Purchase Invoice", "index": 12},
{"doctype": "Delivery Note", "index": 13},
{"doctype": "Stock Entry", "index": 14},
{"doctype": "Material Request", "index": 15},
{"doctype": "Delivery Trip", "index": 16},
{"doctype": "Pick List", "index": 17},
{"doctype": "Salary Slip", "index": 18},
{"doctype": "Leave Application", "index": 19},
{"doctype": "Expense Claim", "index": 20},
{"doctype": "Payment Entry", "index": 21},
{"doctype": "Lead", "index": 22},
{"doctype": "Opportunity", "index": 23},
{"doctype": "Item Price", "index": 24},
{"doctype": "Purchase Taxes and Charges Template", "index": 25},
{"doctype": "Sales Taxes and Charges", "index": 26},
{"doctype": "Asset", "index": 27},
{"doctype": "Project", "index": 28},
{"doctype": "Task", "index": 29},
{"doctype": "Timesheet", "index": 30},
{"doctype": "Issue", "index": 31},
{"doctype": "Serial No", "index": 32},
{"doctype": "Batch", "index": 33},
{"doctype": "Branch", "index": 34},
{"doctype": "Department", "index": 35},
{"doctype": "Employee Grade", "index": 36},
{"doctype": "Designation", "index": 37},
{"doctype": "Job Opening", "index": 38},
{"doctype": "Job Applicant", "index": 39},
{"doctype": "Job Offer", "index": 40},
{"doctype": "Salary Structure Assignment", "index": 41},
{"doctype": "Appraisal", "index": 42},
{"doctype": "Loan", "index": 43},
{"doctype": "Maintenance Schedule", "index": 44},
{"doctype": "Maintenance Visit", "index": 45},
{"doctype": "Warranty Claim", "index": 46},
{"doctype": "Purchase Order", "index": 11},
{"doctype": "Purchase Receipt", "index": 12},
{"doctype": "Purchase Invoice", "index": 13},
{"doctype": "Delivery Note", "index": 14},
{"doctype": "Stock Entry", "index": 15},
{"doctype": "Material Request", "index": 16},
{"doctype": "Delivery Trip", "index": 17},
{"doctype": "Pick List", "index": 18},
{"doctype": "Salary Slip", "index": 19},
{"doctype": "Leave Application", "index": 20},
{"doctype": "Expense Claim", "index": 21},
{"doctype": "Payment Entry", "index": 22},
{"doctype": "Lead", "index": 23},
{"doctype": "Opportunity", "index": 24},
{"doctype": "Item Price", "index": 25},
{"doctype": "Purchase Taxes and Charges Template", "index": 26},
{"doctype": "Sales Taxes and Charges", "index": 27},
{"doctype": "Asset", "index": 28},
{"doctype": "Project", "index": 29},
{"doctype": "Task", "index": 30},
{"doctype": "Timesheet", "index": 31},
{"doctype": "Issue", "index": 32},
{"doctype": "Serial No", "index": 33},
{"doctype": "Batch", "index": 34},
{"doctype": "Branch", "index": 35},
{"doctype": "Department", "index": 36},
{"doctype": "Employee Grade", "index": 37},
{"doctype": "Designation", "index": 38},
{"doctype": "Job Opening", "index": 39},
{"doctype": "Job Applicant", "index": 40},
{"doctype": "Job Offer", "index": 41},
{"doctype": "Salary Structure Assignment", "index": 42},
{"doctype": "Appraisal", "index": 43},
{"doctype": "Loan", "index": 44},
{"doctype": "Maintenance Schedule", "index": 45},
{"doctype": "Maintenance Visit", "index": 46},
{"doctype": "Warranty Claim", "index": 47},
],
"Healthcare": [
{'doctype': 'Patient', 'index': 1},

View File

@ -57,7 +57,6 @@
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details",
"leave_policy",
"attendance_device_id",
"column_break_44",
"holiday_list",
@ -411,14 +410,6 @@
"oldfieldtype": "Link",
"options": "Branch"
},
{
"fetch_from": "grade.default_leave_policy",
"fetch_if_empty": 1,
"fieldname": "leave_policy",
"fieldtype": "Link",
"label": "Leave Policy",
"options": "Leave Policy"
},
{
"description": "Applicable Holiday List",
"fieldname": "holiday_list",
@ -672,10 +663,10 @@
"oldfieldtype": "Date"
},
{
"depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
"mandatory_depends_on": "eval:doc.status == \"Left\"",
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@ -822,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2020-10-06 15:58:23.805489",
"modified": "2020-10-16 15:02:04.283657",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -15,11 +15,16 @@ frappe.ui.form.on('Employee Advance', {
});
frm.set_query("advance_account", function() {
if (!frm.doc.employee) {
frappe.msgprint(__("Please select employee first"));
}
var company_currency = erpnext.get_currency(frm.doc.company);
return {
filters: {
"root_type": "Asset",
"is_group": 0,
"company": frm.doc.company
"company": frm.doc.company,
"account_currency": ["in", [frm.doc.currency, company_currency]],
}
};
});
@ -63,7 +68,7 @@ frappe.ui.form.on('Employee Advance', {
}, __('Create'));
}else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){
frm.add_custom_button(__("Deduction from salary"), function() {
frm.events.make_deduction_via_additional_salary(frm)
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}
}
@ -127,7 +132,9 @@ frappe.ui.form.on('Employee Advance', {
'employee_advance_name': frm.doc.name,
'return_amount': flt(frm.doc.paid_amount - frm.doc.claimed_amount),
'advance_account': frm.doc.advance_account,
'mode_of_payment': frm.doc.mode_of_payment
'mode_of_payment': frm.doc.mode_of_payment,
'currency': frm.doc.currency,
'exchange_rate': frm.doc.exchange_rate
},
callback: function(r) {
const doclist = frappe.model.sync(r.message);
@ -138,16 +145,72 @@ frappe.ui.form.on('Employee Advance', {
employee: function (frm) {
if (frm.doc.employee) {
return frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount",
args: {
"employee": frm.doc.employee,
"posting_date": frm.doc.posting_date
},
callback: function(r) {
frm.set_value("pending_amount",r.message);
}
});
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
]);
}
},
get_pending_amount: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount",
args: {
"employee": frm.doc.employee,
"posting_date": frm.doc.posting_date
},
callback: function(r) {
frm.set_value("pending_amount", r.message);
}
});
},
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
frm.refresh_fields();
}
}
});
},
currency: function(frm) {
var from_currency = frm.doc.currency;
var company_currency;
if (!frm.doc.company) {
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
} else {
company_currency = erpnext.get_currency(frm.doc.company);
}
if (from_currency != company_currency) {
frm.events.set_exchange_rate(frm, from_currency, company_currency);
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
}
frm.refresh_fields();
},
set_exchange_rate: function(frm, from_currency, company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: from_currency,
to_currency: company_currency,
},
callback: function(r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ " = [?] " + company_currency);
}
});
}
});

View File

@ -13,6 +13,8 @@
"department",
"column_break_4",
"posting_date",
"currency",
"exchange_rate",
"repay_unclaimed_amount_from_salary",
"section_break_8",
"purpose",
@ -91,7 +93,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Advance Amount",
"options": "Company:company:default_currency",
"options": "currency",
"reqd": 1
},
{
@ -99,7 +101,7 @@
"fieldtype": "Currency",
"label": "Paid Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"options": "currency",
"read_only": 1
},
{
@ -107,7 +109,7 @@
"fieldtype": "Currency",
"label": "Claimed Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"options": "currency",
"read_only": 1
},
{
@ -161,7 +163,7 @@
"fieldname": "return_amount",
"fieldtype": "Currency",
"label": "Returned Amount",
"options": "Company:company:default_currency",
"options": "currency",
"read_only": 1
},
{
@ -175,13 +177,31 @@
"fieldname": "pending_amount",
"fieldtype": "Currency",
"label": "Pending Amount",
"options": "Company:company:default_currency",
"options": "currency",
"read_only": 1
},
{
"default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"reqd": 1
},
{
"depends_on": "currency",
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-12 12:42:39.833818",
"modified": "2020-11-25 12:01:55.980721",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",

View File

@ -19,7 +19,6 @@ class EmployeeAdvance(Document):
def validate(self):
self.set_status()
self.validate_employee_advance_account()
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@ -38,16 +37,9 @@ class EmployeeAdvance(Document):
elif self.docstatus == 2:
self.status = "Cancelled"
def validate_employee_advance_account(self):
company_currency = erpnext.get_company_currency(self.company)
if (self.advance_account and
company_currency != frappe.db.get_value('Account', self.advance_account, 'account_currency')):
frappe.throw(_("Advance account currency should be same as company currency {0}")
.format(company_currency))
def set_total_advance_paid(self):
paid_amount = frappe.db.sql("""
select ifnull(sum(debit_in_account_currency), 0) as paid_amount
select ifnull(sum(debit), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = 'Employee Advance'
and against_voucher = %s
@ -56,7 +48,7 @@ class EmployeeAdvance(Document):
""", (self.name, self.employee), as_dict=1)[0].paid_amount
return_amount = frappe.db.sql("""
select name, ifnull(sum(credit_in_account_currency), 0) as return_amount
select ifnull(sum(credit), 0) as return_amount
from `tabGL Entry`
where against_voucher_type = 'Employee Advance'
and voucher_type != 'Expense Claim'
@ -65,6 +57,11 @@ class EmployeeAdvance(Document):
and party = %s
""", (self.name, self.employee), as_dict=1)[0].return_amount
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
if return_amount != 0:
return_amount = flt(return_amount) / flt(self.exchange_rate)
if flt(paid_amount) > self.advance_amount:
frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"),
EmployeeAdvanceOverPayment)
@ -107,16 +104,27 @@ def make_bank_entry(dt, dn):
doc = frappe.get_doc(dt, dn)
payment_account = get_default_bank_cash_account(doc.company, account_type="Cash",
mode_of_payment=doc.mode_of_payment)
if not payment_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency')
advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc )
paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc)
je = frappe.new_doc("Journal Entry")
je.posting_date = nowdate()
je.voucher_type = 'Bank Entry'
je.company = doc.company
je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose
je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0
je.append("accounts", {
"account": doc.advance_account,
"debit_in_account_currency": flt(doc.advance_amount),
"account_currency": advance_account_currency,
"exchange_rate": flt(advance_exchange_rate),
"debit_in_account_currency": flt(advance_amount),
"reference_type": "Employee Advance",
"reference_name": doc.name,
"party_type": "Employee",
@ -128,19 +136,41 @@ def make_bank_entry(dt, dn):
je.append("accounts", {
"account": payment_account.account,
"cost_center": erpnext.get_default_cost_center(doc.company),
"credit_in_account_currency": flt(doc.advance_amount),
"credit_in_account_currency": flt(paying_amount),
"account_currency": payment_account.account_currency,
"account_type": payment_account.account_type
"account_type": payment_account.account_type,
"exchange_rate": flt(paying_exchange_rate)
})
return je.as_dict()
def get_advance_amount_advance_exchange_rate(advance_account_currency, doc):
if advance_account_currency != doc.currency:
advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
advance_exchange_rate = 1
else:
advance_amount = doc.advance_amount
advance_exchange_rate = doc.exchange_rate
return advance_amount, advance_exchange_rate
def get_paying_amount_paying_exchange_rate(payment_account, doc):
if payment_account.account_currency != doc.currency:
paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
paying_exchange_rate = 1
else:
paying_amount = doc.advance_amount
paying_exchange_rate = doc.exchange_rate
return paying_amount, paying_exchange_rate
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency
additional_salary.amount = doc.paid_amount - doc.claimed_amount
additional_salary.company = doc.company
additional_salary.ref_doctype = doc.doctype
@ -149,26 +179,28 @@ def create_return_through_additional_salary(doc):
return additional_salary
@frappe.whitelist()
def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None):
return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
mode_of_payment_type = ''
if mode_of_payment:
mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
if mode_of_payment_type not in ["Cash", "Bank"]:
# if mode of payment is General then it unset the type
mode_of_payment_type = None
def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None):
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate()
# if mode of payment is Bank then voucher type is Bank Entry
je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry'
je.voucher_type = get_voucher_type(mode_of_payment)
je.company = company
je.remark = 'Return against Employee Advance: ' + employee_advance_name
je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0
advance_account_amount = flt(return_amount) if advance_account_currency==currency \
else flt(return_amount) * flt(exchange_rate)
je.append('accounts', {
'account': advance_account,
'credit_in_account_currency': return_amount,
'credit_in_account_currency': advance_account_amount,
'account_currency': advance_account_currency,
'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1,
'reference_type': 'Employee Advance',
'reference_name': employee_advance_name,
'party_type': 'Employee',
@ -176,13 +208,25 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
'is_advance': 'Yes'
})
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
else flt(return_amount) * flt(exchange_rate)
je.append("accounts", {
"account": return_account.account,
"debit_in_account_currency": return_amount,
"account_currency": return_account.account_currency,
"account_type": return_account.account_type
"account": bank_cash_account.account,
"debit_in_account_currency": bank_amount,
"account_currency": bank_cash_account.account_currency,
"account_type": bank_cash_account.account_type,
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
})
return je.as_dict()
def get_voucher_type(mode_of_payment=None):
voucher_type = "Cash Entry"
if mode_of_payment:
mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry"
return voucher_type

View File

@ -3,15 +3,17 @@
# See license.txt
from __future__ import unicode_literals
import frappe
import frappe, erpnext
import unittest
from frappe.utils import nowdate
from erpnext.hr.doctype.employee_advance.employee_advance import make_bank_entry
from erpnext.hr.doctype.employee_advance.employee_advance import EmployeeAdvanceOverPayment
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeAdvance(unittest.TestCase):
def test_paid_amount_and_status(self):
advance = make_employee_advance()
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name)
journal_entry = make_payment_entry(advance)
journal_entry.submit()
@ -33,11 +35,13 @@ def make_payment_entry(advance):
return journal_entry
def make_employee_advance():
def make_employee_advance(employee_name):
doc = frappe.new_doc("Employee Advance")
doc.employee = "_T-Employee-00001"
doc.employee = employee_name
doc.company = "_Test company"
doc.purpose = "For site visit"
doc.currency = erpnext.get_company_currency("_Test company")
doc.exchange_rate = 1
doc.advance_amount = 1000
doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC"

View File

@ -1,167 +1,69 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2018-04-13 16:14:24.174138",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2018-04-13 16:14:24.174138",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"default_salary_structure"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_leave_policy",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Leave Policy",
"length": 0,
"no_copy": 0,
"options": "Leave Policy",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_salary_structure",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Salary Structure",
"length": 0,
"no_copy": 0,
"options": "Salary Structure",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Salary Structure"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-09-18 17:17:45.617624",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-26 13:12:07.815330",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grade",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -7,6 +7,7 @@ import unittest
from frappe.utils import random_string, nowdate
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.hr.doctype.employee.test_employee import make_employee
test_records = frappe.get_test_records('Expense Claim')
test_dependencies = ['Employee']
@ -126,6 +127,9 @@ def generate_taxes():
def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None):
employee = frappe.db.get_value("Employee", {"status": "Active"})
if not employee:
employee = make_employee("test_employee@expense_claim.com", company=company)
currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
expense_claim = {
"doctype": "Expense Claim",

View File

@ -71,9 +71,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"oldfieldname": "tax_amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
"options": "currency"
},
{
"columns": 2,
@ -81,9 +79,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total",
"oldfieldname": "total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"options": "currency",
"read_only": 1
},
{
@ -106,7 +102,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-05-11 19:01:26.611758",
"modified": "2020-09-23 20:27:36.027728",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Taxes and Charges",

View File

@ -21,6 +21,7 @@
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
"automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@ -41,7 +42,7 @@
"description": "Employee records are created using the selected field",
"fieldname": "emp_created_by",
"fieldtype": "Select",
"label": "Employee Records to Be Created By",
"label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@ -117,7 +118,7 @@
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Applications"
"label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
@ -125,13 +126,19 @@
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
},
{
"default": "0",
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
"fieldtype": "Check",
"label": "Automatically Allocate Leaves Based On Leave Policy"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 11:49:46.168027",
"modified": "2020-08-27 14:30:28.995324",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-02-20 19:10:38",
@ -24,6 +25,7 @@
"compensatory_request",
"leave_period",
"leave_policy",
"leave_policy_assignment",
"carry_forwarded_leaves_count",
"expired",
"amended_from",
@ -160,9 +162,10 @@
"read_only": 1
},
{
"fetch_from": "employee.leave_policy",
"fetch_from": "leave_policy_assignment.leave_policy",
"fieldname": "leave_policy",
"fieldtype": "Link",
"hidden": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
@ -209,12 +212,21 @@
"fieldtype": "Float",
"label": "Carry Forwarded Leaves",
"read_only": 1
},
{
"fieldname": "leave_policy_assignment",
"fieldtype": "Link",
"label": "Leave Policy Assignment",
"options": "Leave Policy Assignment",
"read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"modified": "2019-08-08 15:08:42.440909",
"links": [],
"modified": "2020-08-20 14:25:10.314323",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",

View File

@ -51,9 +51,19 @@ class LeaveAllocation(Document):
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
if self.leave_policy_assignment:
self.update_leave_policy_assignments_when_no_allocations_left()
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1,
"leave_policy_assignment": self.leave_policy_assignment
})
if len(allocations) == 0:
frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0)
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
frappe.throw(_("To date cannot be before from date"))

View File

@ -130,8 +130,7 @@ class LeaveApplication(Document):
if self.status == "Approved":
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
@ -293,7 +292,8 @@ class LeaveApplication(Document):
def set_half_day_date(self):
if self.from_date == self.to_date and self.half_day == 1:
self.half_day_date = self.from_date
elif self.half_day == 0:
if self.half_day == 0:
self.half_day_date = None
def notify_employee(self):
@ -376,24 +376,32 @@ class LeaveApplication(Document):
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
raise_exception = True
if frappe.flags.in_patch:
raise_exception=False
args = dict(
leaves=self.total_leave_days * -1,
from_date=self.from_date,
to_date=self.to_date,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee)
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
''' splits leave application into two ledger entries to consider expiry of allocation '''
raise_exception = True
if frappe.flags.in_patch:
raise_exception=False
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee),
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)

View File

@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
test_dependencies = ["Leave Allocation", "Leave Block List"]
@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leaves_creation(self):
frappe.db.sql('''delete from `tabLeave Period`''')
frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
leave_period = get_leave_period()
employee = get_employee()
leave_type = 'Test Earned Leave Type'
if not frappe.db.exists('Leave Type', leave_type):
frappe.get_doc(dict(
leave_type_name = leave_type,
doctype = 'Leave Type',
is_earned_leave = 1,
earned_leave_frequency = 'Monthly',
rounding = 0.5,
max_leaves_allowed = 6
)).insert()
frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1)
frappe.get_doc(dict(
leave_type_name = leave_type,
doctype = 'Leave Type',
is_earned_leave = 1,
earned_leave_frequency = 'Monthly',
rounding = 0.5,
max_leaves_allowed = 6
)).insert()
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
}).insert()
frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name)
allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12)
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves
i = 0

View File

@ -22,7 +22,12 @@ frappe.ui.form.on('Leave Encashment', {
}
},
employee: function(frm) {
frm.trigger("get_leave_details_for_encashment");
if (frm.doc.employee) {
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_leave_details_for_encashment')
]);
}
},
leave_type: function(frm) {
frm.trigger("get_leave_details_for_encashment");
@ -40,5 +45,20 @@ frappe.ui.form.on('Leave Encashment', {
}
});
}
}
},
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
frm.refresh_fields();
}
}
});
},
});

View File

@ -12,6 +12,7 @@
"employee",
"employee_name",
"department",
"company",
"column_break_4",
"leave_type",
"leave_allocation",
@ -19,9 +20,11 @@
"encashable_days",
"amended_from",
"payroll",
"encashment_amount",
"encashment_date",
"additional_salary"
"additional_salary",
"column_break_14",
"currency",
"encashment_amount"
],
"fields": [
{
@ -109,6 +112,7 @@
"in_list_view": 1,
"label": "Encashment Amount",
"no_copy": 1,
"options": "currency",
"read_only": 1
},
{
@ -124,11 +128,34 @@
"no_copy": 1,
"options": "Additional Salary",
"read_only": 1
},
{
"default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2019-12-16 11:51:57.732223",
"modified": "2020-11-25 11:56:06.777241",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",

View File

@ -16,10 +16,16 @@ class LeaveEncashment(Document):
def validate(self):
set_employee_name(self)
self.get_leave_details_for_encashment()
self.validate_salary_structure()
if not self.encashment_date:
self.encashment_date = getdate(nowdate())
def validate_salary_structure(self):
if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}):
frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee))
def before_submit(self):
if self.encashment_amount <= 0:
frappe.throw(_("You can only submit Leave Encashment for a valid encashment amount"))
@ -30,6 +36,7 @@ class LeaveEncashment(Document):
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.company = frappe.get_value("Employee", self.employee, "company")
additional_salary.employee = self.employee
additional_salary.currency = self.currency
earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component")
if not earning_component:
frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type))

View File

@ -9,6 +9,7 @@ from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
frappe.db.sql('''delete from `tabAdditional Salary`''')
@ -29,14 +31,26 @@ class TestLeaveEncashment(unittest.TestCase):
# create employee, salary structure and assignment
self.employee = make_employee("test_employee_encashment@example.com")
frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": self.leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data))
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
# create the leave period and assign the leaves
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
self.leave_period.grant_leave_allocation(employee=self.employee)
#grant Leaves
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
def tearDown(self):
for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
frappe.db.sql("delete from `tab%s`" % dt)
def test_leave_balance_value_and_amount(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')
@ -45,7 +59,8 @@ class TestLeaveEncashment(unittest.TestCase):
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
payroll_date=today(),
currency="INR"
)).insert()
self.assertEqual(leave_encashment.leave_balance, 10)
@ -65,7 +80,8 @@ class TestLeaveEncashment(unittest.TestCase):
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
payroll_date=today(),
currency="INR"
)).insert()
leave_encashment.submit()

View File

@ -2,14 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Period', {
refresh: (frm)=>{
frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0);
if(!frm.is_new()) {
frm.add_custom_button(__('Grant Leaves'), function () {
frm.trigger("grant_leaves");
});
}
},
from_date: (frm)=>{
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', {
"filters": {
"company": frm.doc.company,
}
}
})
},
grant_leaves: function(frm) {
var d = new frappe.ui.Dialog({
title: __('Grant Leaves'),
fields: [
{
"label": "Filter Employees By (Optional)",
"fieldname": "sec_break",
"fieldtype": "Section Break",
},
{
"label": "Employee Grade",
"fieldname": "grade",
"fieldtype": "Link",
"options": "Employee Grade"
},
{
"label": "Department",
"fieldname": "department",
"fieldtype": "Link",
"options": "Department"
},
{
"fieldname": "col_break",
"fieldtype": "Column Break",
},
{
"label": "Designation",
"fieldname": "designation",
"fieldtype": "Link",
"options": "Designation"
},
{
"label": "Employee",
"fieldname": "employee",
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "sec_break",
"fieldtype": "Section Break",
},
{
"label": "Add unused leaves from previous allocations",
"fieldname": "carry_forward",
"fieldtype": "Check"
}
],
primary_action: function() {
var data = d.get_values();
frappe.call({
doc: frm.doc,
method: "grant_leave_allocation",
args: data,
callback: function(r) {
if(!r.exc) {
d.hide();
frm.reload_doc();
}
}
});
},
primary_action_label: __('Grant')
};
});
d.show();
}
},
});

View File

@ -7,24 +7,10 @@ import frappe
from frappe import _
from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document
from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
from erpnext.hr.utils import validate_overlap
from frappe.utils.background_jobs import enqueue
from six import iteritems
class LeavePeriod(Document):
def get_employees(self, args):
conditions, values = [], []
for field, value in iteritems(args):
if value:
conditions.append("{0}=%s".format(field))
values.append(value)
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
.format(condition=condition_str), tuple(values)))
return employees
def validate(self):
self.validate_dates()
@ -33,96 +19,3 @@ class LeavePeriod(Document):
def validate_dates(self):
if getdate(self.from_date) >= getdate(self.to_date):
frappe.throw(_("To date can not be equal or less than from date"))
def grant_leave_allocation(self, grade=None, department=None, designation=None,
employee=None, carry_forward=0):
employee_records = self.get_employees({
"grade": grade,
"department": department,
"designation": designation,
"name": employee
})
if employee_records:
if len(employee_records) > 20:
frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
else:
grant_leave_alloc_for_employees(employee_records, self, carry_forward)
else:
frappe.msgprint(_("No Employee Found"))
def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
leave_allocations = []
existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
leave_type_details = get_leave_type_details()
count = 0
for employee in employee_records.keys():
if employee in existing_allocations_for:
continue
count +=1
leave_policy = get_employee_leave_policy(employee)
if leave_policy:
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
leave_allocations.append(leave_allocation)
frappe.db.commit()
frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
if leave_allocations:
frappe.msgprint(_("Leaves has been granted sucessfully"))
def get_existing_allocations(employees, leave_period):
leave_allocations = frappe.db.sql_list("""
SELECT DISTINCT
employee
FROM `tabLeave Allocation`
WHERE
leave_period=%s
AND employee in (%s)
AND carry_forward=0
AND docstatus=1
""" % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
if leave_allocations:
frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
.format("\n".join(leave_allocations)))
return leave_allocations
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
''' Creates leave allocation for the given employee in the provided leave period '''
if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(leave_period.from_date):
remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
allocation = frappe.get_doc(dict(
doctype="Leave Allocation",
employee=employee,
leave_type=leave_type,
from_date=leave_period.from_date,
to_date=leave_period.to_date,
new_leaves_allocated=new_leaves_allocated,
leave_period=leave_period.name,
carry_forward=carry_forward
))
allocation.save(ignore_permissions = True)
allocation.submit()
return allocation.name

View File

@ -5,43 +5,11 @@ from __future__ import unicode_literals
import frappe, erpnext
import unittest
from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
test_dependencies = ["Employee", "Leave Type", "Leave Policy"]
class TestLeavePeriod(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabLeave Period`")
def test_leave_grant(self):
leave_type = "_Test Leave Type"
# create the leave policy
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{
"leave_type": leave_type,
"annual_allocation": 20
}]
}).insert()
leave_policy.submit()
# create employee and assign the leave period
employee = "test_leave_period@employee.com"
employee_doc_name = make_employee(employee)
frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name)
# clear the already allocated leave
frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com")
# create the leave period
leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
# test leave_allocation
leave_period.grant_leave_allocation(employee=employee_doc_name)
self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20)
pass
def create_leave_period(from_date, to_date, company=None):
leave_period = frappe.db.get_value('Leave Period',

View File

@ -0,0 +1,72 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Leave Policy Assignment', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
},
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
frm.add_custom_button(__("Grant Leave"), function() {
frappe.call({
doc: frm.doc,
method: "grant_leave_alloc_for_employee",
callback: function(r) {
let leave_allocations = r.message;
let msg = frm.events.get_success_message(leave_allocations);
frappe.msgprint(msg);
cur_frm.refresh();
}
});
});
}
},
get_success_message: function(leave_allocations) {
let msg = __("Leaves has been granted successfully");
msg += "<br><table class='table table-bordered'>";
msg += "<tr><th>"+__('Leave Type')+"</th><th>"+__("Leave Allocation")+"</th><th>"+__("Leaves Granted")+"</th><tr>";
for (let key in leave_allocations) {
msg += "<tr><th>"+key+"</th><td>"+leave_allocations[key]["name"]+"</td><td>"+leave_allocations[key]["leaves"]+"</td></tr>";
}
msg += "</table>";
return msg;
},
assignment_based_on: function(frm) {
if (frm.doc.assignment_based_on) {
frm.events.set_effective_date(frm);
} else {
frm.set_value("effective_from", '');
frm.set_value("effective_to", '');
}
},
leave_period: function(frm) {
if (frm.doc.leave_period) {
frm.events.set_effective_date(frm);
}
},
set_effective_date: function(frm) {
if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) {
frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () {
let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date");
let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date");
frm.set_value("effective_from", from_date);
frm.set_value("effective_to", to_date);
});
} else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) {
frappe.model.with_doc("Employee", frm.doc.employee, function () {
let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining");
frm.set_value("effective_from", from_date);
frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12));
});
}
frm.refresh();
}
});

View File

@ -0,0 +1,160 @@
{
"actions": [],
"autoname": "HR-LPOL-ASSGN-.#####",
"creation": "2020-08-19 13:02:43.343666",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"company",
"leave_policy",
"carry_forward",
"column_break_5",
"assignment_based_on",
"leave_period",
"effective_from",
"effective_to",
"leaves_allocated",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee name",
"read_only": 1
},
{
"fieldname": "leave_policy",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
"reqd": 1
},
{
"fieldname": "assignment_based_on",
"fieldtype": "Select",
"label": "Assignment based on",
"options": "\nLeave Period\nJoining Date"
},
{
"depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"fieldname": "leave_period",
"fieldtype": "Link",
"label": "Leave Period",
"mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"options": "Leave Period"
},
{
"fieldname": "effective_from",
"fieldtype": "Date",
"label": "Effective From",
"read_only_depends_on": "eval:doc.assignment_based_on",
"reqd": 1
},
{
"fieldname": "effective_to",
"fieldtype": "Date",
"label": "Effective To",
"read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"reqd": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Leave Policy Assignment",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "carry_forward",
"fieldtype": "Check",
"label": "Add unused leaves from previous allocations"
},
{
"default": "0",
"fieldname": "leaves_allocated",
"fieldtype": "Check",
"hidden": 1,
"label": "Leaves Allocated"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-10-15 15:18:15.227848",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate
from math import ceil
import json
from six import string_types
class LeavePolicyAssignment(Document):
def validate(self):
self.validate_policy_assignment_overlap()
self.set_dates()
def set_dates(self):
if self.assignment_based_on == "Leave Period":
self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
elif self.assignment_based_on == "Joining Date":
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
def validate_policy_assignment_overlap(self):
leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = {
"employee": self.employee,
"name": ("!=", self.name),
"docstatus": 1,
"effective_to": (">=", self.effective_from),
"effective_from": ("<=", self.effective_to)
})
if len(leave_policy_assignments):
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
else:
leave_allocations = {}
leave_type_details = get_leave_type_details()
leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation, new_leaves_allocated = self.create_leave_allocation(
leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
leave_type_details, date_of_joining
)
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
self.db_set("leaves_allocated", 1)
return leave_allocations
def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
# Creates leave allocation for the given employee in the provided leave period
carry_forward = self.carry_forward
if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated,
leave_type_details, date_of_joining)
allocation = frappe.get_doc(dict(
doctype="Leave Allocation",
employee=self.employee,
leave_type=leave_type,
from_date=self.effective_from,
to_date=self.effective_to,
new_leaves_allocated=new_leaves_allocated,
leave_period=self.leave_period or None,
leave_policy_assignment = self.name,
leave_policy = self.leave_policy,
carry_forward=carry_forward
))
allocation.save(ignore_permissions = True)
allocation.submit()
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
return new_leaves_allocated
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
not_granted = []
for assignment in leave_policy_assignments:
try:
frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
except Exception:
not_granted.append(assignment)
if len(not_granted):
msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
else:
msg = _("Leave granted Successfully")
frappe.msgprint(msg)
@frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data):
if isinstance(employees, string_types):
employees= json.loads(employees)
if isinstance(data, string_types):
data = frappe._dict(json.loads(data))
docs_name = []
for employee in employees:
assignment = frappe.new_doc("Leave Policy Assignment")
assignment.employee = employee
assignment.assignment_based_on = data.assignment_based_on or None
assignment.leave_policy = data.leave_policy
assignment.effective_from = getdate(data.effective_from) or None
assignment.effective_to = getdate(data.effective_to) or None
assignment.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward
assignment.save()
assignment.submit()
docs_name.append(assignment.name)
return docs_name
def automatically_allocate_leaves_based_on_leave_policy():
today = getdate()
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
)
pending_assignments = frappe.get_list(
"Leave Policy Assignment",
filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
)
if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
for assignment in pending_assignments:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details

View File

@ -0,0 +1,138 @@
frappe.listview_settings['Leave Policy Assignment'] = {
onload: function (list_view) {
let me = this;
list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () {
me.dialog = new frappe.ui.form.MultiSelectDialog({
doctype: "Employee",
target: cur_list,
setters: {
company: '',
department: '',
},
data_fields: [{
fieldname: 'leave_policy',
fieldtype: 'Link',
options: 'Leave Policy',
label: __('Leave Policy'),
reqd: 1
},
{
fieldname: 'assignment_based_on',
fieldtype: 'Select',
options: ["", "Leave Period"],
label: __('Assignment Based On'),
onchange: () => {
if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") {
cur_dialog.set_df_property("effective_from", "read_only", 1);
cur_dialog.set_df_property("leave_period", "reqd", 1);
cur_dialog.set_df_property("effective_to", "read_only", 1);
} else {
cur_dialog.set_df_property("effective_from", "read_only", 0);
cur_dialog.set_df_property("leave_period", "reqd", 0);
cur_dialog.set_df_property("effective_to", "read_only", 0);
cur_dialog.set_value("effective_from", "");
cur_dialog.set_value("effective_to", "");
}
}
},
{
fieldname: "leave_period",
fieldtype: 'Link',
options: "Leave Period",
label: __('Leave Period'),
depends_on: doc => {
return doc.assignment_based_on == 'Leave Period';
},
onchange: () => {
if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date();
}
}
},
{
fieldtype: "Column Break"
},
{
fieldname: 'effective_from',
fieldtype: 'Date',
label: __('Effective From'),
reqd: 1
},
{
fieldname: 'effective_to',
fieldtype: 'Date',
label: __('Effective To'),
reqd: 1
},
{
fieldname: 'carry_forward',
fieldtype: 'Check',
label: __('Add unused leaves from previous allocations')
}
],
get_query() {
return {
filters: {
status: ['=', 'Active']
}
};
},
add_filters_group: 1,
primary_action_label: "Assign",
action(employees, data) {
frappe.call({
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees',
async: false,
args: {
employees: employees,
data: data
}
});
cur_dialog.hide();
}
});
});
list_view.page.add_inner_button(__("Grant Leaves"), function () {
me.dialog = new frappe.ui.form.MultiSelectDialog({
doctype: "Leave Policy Assignment",
target: cur_list,
setters: {
company: '',
employee: '',
},
get_query() {
return {
filters: {
docstatus: ['=', 1],
leaves_allocated: ['=', 0]
}
};
},
add_filters_group: 1,
primary_action_label: "Grant Leaves",
action(leave_policy_assignments) {
frappe.call({
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
async: false,
args: {
leave_policy_assignments: leave_policy_assignments
}
});
me.dialog.hide();
}
});
});
},
set_effective_date: function () {
if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) {
frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () {
let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date");
let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date");
cur_dialog.set_value("effective_from", from_date);
cur_dialog.set_value("effective_to", to_date);
});
}
}
};

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
def test_grant_leaves(self):
leave_period = get_leave_period()
employee = get_employee()
# create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy()
leave_policy.submit()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name,
"leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
leave_period = get_leave_period()
employee = get_employee()
# create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy()
leave_policy.submit()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
# every leave is allocated no more leave can be granted now
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name,
"leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
# User all allowed to grant leave when there is no allocation against assignment
leave_alloc_doc.cancel()
leave_alloc_doc.delete()
leave_policy_assignment_doc.reload()
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
def tearDown(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec

View File

@ -15,6 +15,8 @@
"column_break_3",
"is_carry_forward",
"is_lwp",
"is_ppl",
"fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"include_holiday",
@ -31,6 +33,7 @@
"is_earned_leave",
"earned_leave_frequency",
"column_break_22",
"based_on_date_of_joining",
"rounding"
],
"fields": [
@ -77,6 +80,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.is_ppl == 0",
"fieldname": "is_lwp",
"fieldtype": "Check",
"label": "Is Leave Without Pay"
@ -183,12 +187,33 @@
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.is_earned_leave",
"description": "If checked, leave will be granted on the day of joining every month.",
"fieldname": "based_on_date_of_joining",
"fieldtype": "Check",
"label": "Based On Date Of Joining"
},
{
"depends_on": "eval:doc.is_lwp == 0",
"fieldname": "is_ppl",
"fieldtype": "Check",
"label": "Is Partially Paid Leave"
},
{
"depends_on": "eval:doc.is_ppl == 1",
"fieldname": "fraction_of_daily_salary_per_leave",
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
"mandatory_depends_on": "eval:doc.is_ppl == 1"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2019-12-12 12:48:37.780254",
"modified": "2020-10-15 15:49:47.555105",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",

View File

@ -21,3 +21,9 @@ class LeaveType(Document):
leave_allocation = [l['name'] for l in leave_allocation]
if leave_allocation:
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
if self.is_lwp and self.is_ppl:
frappe.throw(_("Leave Type can be either without pay or partial pay"))
if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))

View File

@ -18,9 +18,14 @@ def create_leave_type(**args):
"allow_encashment": args.allow_encashment or 0,
"is_earned_leave": args.is_earned_leave or 0,
"is_lwp": args.is_lwp or 0,
"is_ppl":args.is_ppl or 0,
"is_carry_forward": args.is_carry_forward or 0,
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment"
})
if leave_type.is_ppl:
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
return leave_type

View File

@ -215,19 +215,6 @@ def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
def get_employee_leave_policy(employee):
leave_policy = frappe.db.get_value("Employee", employee, "leave_policy")
if not leave_policy:
employee_grade = frappe.db.get_value("Employee", employee, "grade")
if employee_grade:
leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy")
if not leave_policy:
frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade))
if leave_policy:
return frappe.get_doc("Leave Policy", leave_policy)
else:
frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee))
def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
existing_record = frappe.db.exists(doctype, {
"payroll_period": payroll_period,
@ -300,43 +287,68 @@ def generate_leave_encashment():
def allocate_earned_leaves():
'''Allocate earned leaves to Employees'''
e_leave_types = frappe.get_all("Leave Type",
fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"],
filters={'is_earned_leave' : 1})
e_leave_types = get_earned_leaves()
today = getdate()
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
for e_leave_type in e_leave_types:
leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
leave_allocations = get_leave_allocations(today, e_leave_type.name)
for allocation in leave_allocations:
leave_policy = get_employee_leave_policy(allocation.employee)
if not leave_policy:
if not allocation.leave_policy_assignment and not allocation.leave_policy:
continue
if not e_leave_type.earned_leave_frequency == "Monthly":
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
continue
leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value(
"Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"])
annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
'parent': leave_policy.name,
'parent': leave_policy,
'leave_type': e_leave_type.name
}, fieldname=['annual_allocation'])
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
from_date=allocation.from_date
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if e_leave_type.based_on_date_of_joining_date:
from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
if new_allocation == allocation.total_leaves_allocated:
continue
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today)
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if new_allocation != allocation.total_leaves_allocated:
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
from `tabLeave Allocation`
where
%s between from_date and to_date and docstatus=1
and leave_type=%s""",
(date, leave_type), as_dict=1)
def get_earned_leaves():
return frappe.get_all("Leave Type",
fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"],
filters={'is_earned_leave' : 1})
def create_additional_leave_ledger_entry(allocation, leaves, date):
''' Create leave ledger entry for leave types '''
@ -345,24 +357,32 @@ def create_additional_leave_ledger_entry(allocation, leaves, date):
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
def check_frequency_hit(from_date, to_date, frequency):
'''Return True if current date matches frequency'''
from_dt = get_datetime(from_date)
to_dt = get_datetime(to_date)
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
import calendar
from dateutil import relativedelta
rd = relativedelta.relativedelta(to_dt, from_dt)
months = rd.months
if frequency == "Quarterly":
if not months % 3:
from_date = get_datetime(from_date)
to_date = get_datetime(to_date)
rd = relativedelta.relativedelta(to_date, from_date)
#last day of month
last_day = calendar.monthrange(to_date.year, to_date.month)[1]
if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
if frequency == "Monthly":
return True
elif frequency == "Half-Yearly":
if not months % 6:
elif frequency == "Quarterly" and rd.months % 3:
return True
elif frequency == "Yearly":
if not months % 12:
elif frequency == "Half-Yearly" and rd.months % 6:
return True
elif frequency == "Yearly" and rd.months % 12:
return True
if frappe.flags.in_test:
return True
return False
def get_salary_assignment(employee, date):
assignment = frappe.db.sql("""
select * from `tabSalary Structure Assignment`
@ -454,3 +474,10 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
def grant_leaves_automatically():
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
if automatically_allocate_leaves_based_on_leave_policy:
lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
for assignment in lpa:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()

View File

@ -26,11 +26,11 @@
"disbursed_amount",
"column_break_11",
"maximum_loan_amount",
"is_term_loan",
"repayment_method",
"repayment_periods",
"monthly_repayment_amount",
"repayment_start_date",
"is_term_loan",
"account_info",
"mode_of_payment",
"payment_account",
@ -332,6 +332,7 @@
"read_only": 1
},
{
"depends_on": "eval:doc.is_secured_loan",
"fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
@ -352,7 +353,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-05 10:04:00.762975",
"modified": "2020-11-24 12:27:23.208240",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -13,6 +13,8 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calcul
class Loan(AccountsController):
def validate(self):
if self.applicant_type == 'Employee' and self.repay_from_salary:
validate_employee_currency_with_company_currency(self.applicant, self.company)
self.set_loan_amount()
self.validate_loan_amount()
self.set_missing_fields()
@ -329,5 +331,14 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a
return unpledge_request
def validate_employee_currency_with_company_currency(applicant, company):
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
frappe.throw(_("Please select Company"))
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))

View File

@ -19,6 +19,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled
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
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
class TestLoan(unittest.TestCase):
def setUp(self):
@ -44,6 +45,7 @@ class TestLoan(unittest.TestCase):
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR')
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee, make_salary_structure
from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts
class TestLoanApplication(unittest.TestCase):
@ -14,6 +14,7 @@ class TestLoanApplication(unittest.TestCase):
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
self.create_loan_application()
def create_loan_application(self):
@ -29,7 +30,6 @@ class TestLoanApplication(unittest.TestCase):
})
loan_application.insert()
def test_loan_totals(self):
loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant})

View File

@ -171,10 +171,10 @@ def get_total_pledged_security_value(loan):
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]
def get_disbursal_amount(loan, on_current_security_price=0):
loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
"maximum_loan_amount"], as_dict=1)
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
@ -188,9 +188,12 @@ def get_disbursal_amount(loan):
- flt(loan_details.total_principal_paid)
security_value = 0.0
if loan_details.is_secured_loan:
if loan_details.is_secured_loan and on_current_security_price:
security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan and not on_current_security_price:
security_value = flt(loan_details.maximum_loan_amount)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)

View File

@ -169,8 +169,8 @@ class BOM(WebsiteGenerator):
'qty' : args.get("qty") or args.get("stock_qty") or 1,
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1),
'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0,
'sourced_by_supplier' : args['sourced_by_supplier'] or 0
'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')),
'sourced_by_supplier' : args.get('sourced_by_supplier', 0)
}
return ret_item

View File

@ -31,6 +31,16 @@ frappe.ui.form.on('Job Card', {
}
}
frm.set_query("quality_inspection", function() {
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
filters: {
"item_code": frm.doc.production_item,
"reference_name": frm.doc.name
}
};
});
frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)

View File

@ -20,6 +20,7 @@
"production_item",
"item_name",
"for_quantity",
"quality_inspection",
"wip_warehouse",
"column_break_12",
"employee",
@ -305,11 +306,19 @@
"label": "Sequence Id",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:!doc.__islocal;",
"fieldname": "quality_inspection",
"fieldtype": "Link",
"label": "Quality Inspection",
"no_copy": 1,
"options": "Quality Inspection"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-10-14 12:58:25.327897",
"modified": "2020-11-19 18:26:50.531664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@ -26,7 +26,7 @@ frappe.query_reports["BOM Stock Report"] = {
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.id == "item") {
if (data["Enough Parts to Build"] > 0) {
if (data["enough_parts_to_build"] > 0) {
value = `<a style='color:green' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
value = `<a style='color:red' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;

View File

@ -25,4 +25,5 @@ Hub Node
Quality Management
Communication
Loan Management
Payroll
Payroll
Telephony

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