diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..24f122a8d43 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..26bb7ab280c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index b603ed5e53d..9cc4663c394 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -21,8 +21,8 @@ def docs_link_exists(body): if word.startswith('http') and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - _, org, repo, _type, ref = parsed_url.path.split('/') - if org == "frappe" and repo in docs_repos: + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True diff --git a/.travis/site_config.json b/.travis/site_config.json index dae80095d45..572bbd08532 100644 --- a/.travis/site_config.json +++ b/.travis/site_config.json @@ -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 } \ No newline at end of file diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 39bf4b053a7..85f54f98ba8 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -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() diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py index 6c83e3bd670..acb11e557a5 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py @@ -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" diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 27546335c91..e9fc5f0a1d6 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -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 diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 8083b21f759..af8940cde5b 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -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, diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py index 3d74d9a3b24..919dd0cba77 100644 --- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py @@ -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", { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d8394785c6b..cd712738aa6 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -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: diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index d3040c8db87..7a06d3572a6 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -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] + ] + }; + }); + }, +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index d51856a8a4b..ee2092adcc8 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -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() diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 54229f52470..bdfe532b9fb 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 11ab02021be..31a4c8a3879 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -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.

\ - 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")) + + "

" + _("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: diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 558e21c13aa..7f4f7554807 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -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')); diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 570111acac7..d856ae34762 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -14,7 +14,6 @@ "column_break_9", "update_stock", "ignore_pricing_rule", - "hide_unavailable_items", "warehouse", "campaign", "company_address", @@ -23,6 +22,9 @@ "section_break_11", "payments", "section_break_14", + "hide_images", + "hide_unavailable_items", + "auto_add_item_to_cart", "item_groups", "column_break_16", "customer_groups", @@ -124,7 +126,8 @@ }, { "fieldname": "section_break_14", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Configuration" }, { "description": "Only show Items from these Item Groups", @@ -314,13 +317,25 @@ "fieldname": "hide_unavailable_items", "fieldtype": "Check", "label": "Hide Unavailable Items" + }, + { + "default": "0", + "fieldname": "hide_images", + "fieldtype": "Check", + "label": "Hide Images" + }, + { + "default": "0", + "fieldname": "auto_add_item_to_cart", + "fieldtype": "Check", + "label": "Automatically Add Filtered Item To Cart" } ], "icon": "icon-cog", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-29 13:18:38.795925", + "modified": "2020-12-10 13:59:28.877572", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index edf86590c8b..62dc1fcb209 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -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) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index cc8ed4bc491..d08a854142a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -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", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ec0a485bfc1..af8d21d9ce4 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -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": diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b003328cc47..2c7cd14451d 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -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: diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 31356c6e8b6..e08a0e5cc2b 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -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" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1d41d0fa2a9..7830cfd3702 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ return (doc.qty<=doc.received_qty) ? "green" : "orange"; }); } + + this.frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); }, onload: function() { this._super(); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2df77a84c79..c64ffd878c4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -127,6 +126,7 @@ "write_off_cost_center", "advances_section", "allocate_advances_automatically", + "adjust_advance_taxes", "get_advances", "advances", "payment_schedule_section", @@ -152,9 +152,11 @@ "is_opening", "against_expense_account", "column_break_63", + "unrealized_profit_loss_account", "status", "inter_company_invoice_reference", "is_internal_supplier", + "represents_company", "remarks", "subscription_section", "from_date", @@ -1223,7 +1225,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, { @@ -1330,13 +1332,37 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "description": "Taxes paid while advance payment will be adjusted against this invoice", + "fieldname": "adjust_advance_taxes", + "fieldtype": "Check", + "label": "Adjust Advance Taxes" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Company which internal supplier represents", + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:18.266978", + "modified": "2020-12-11 12:46:12.796378", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 91c4dfb5877..d94d261c6bc 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -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) @@ -201,8 +206,8 @@ class PurchaseInvoice(BuyingController): ["Purchase Receipt", "purchase_receipt", "pr_detail"] ]) - def validate_warehouse(self): - if self.update_stock: + def validate_warehouse(self, for_validate=True): + if self.update_stock and for_validate: for d in self.get('items'): if not d.warehouse: frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). @@ -228,7 +233,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock: self.validate_item_code() - self.validate_warehouse() + self.validate_warehouse(for_validate) if auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -444,6 +449,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -452,7 +458,6 @@ class PurchaseInvoice(BuyingController): self.make_payment_gl_entries(gl_entries) self.make_write_off_gl_entry(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries) - return gl_entries def check_asset_cwip_enabled(self): @@ -469,31 +474,30 @@ class PurchaseInvoice(BuyingController): # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: - # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) - ) + if grand_total and not self.is_internal_transfer(): + # Didnot use base_grand_total to book rounding loss gle + grand_total_in_company_currency = flt(grand_total * self.conversion_rate, + self.precision("grand_total")) + gl_entries.append( + self.get_gl_dict({ + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": self.against_expense_account, + "credit": grand_total_in_company_currency, + "credit_in_account_currency": grand_total_in_company_currency \ + if self.party_account_currency==self.company_currency else grand_total, + "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center + }, self.party_account_currency, item=self) + ) def make_item_gl_entries(self, gl_entries): # item gl entries stock_items = self.get_stock_items() - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") if self.update_stock and self.auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -521,7 +525,6 @@ class PurchaseInvoice(BuyingController): item, voucher_wise_stock_value, account_currency) if item.from_warehouse: - gl_entries.append(self.get_gl_dict({ "account": warehouse_account[item.warehouse]['account'], "against": warehouse_account[item.from_warehouse]["account"], @@ -541,16 +544,18 @@ class PurchaseInvoice(BuyingController): "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) - ) + # Do not book expense for transfer within same company transfer + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) else: gl_entries.append( @@ -827,7 +832,8 @@ class PurchaseInvoice(BuyingController): }, account_currency, item=tax) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \ + and not self.is_internal_transfer(): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) valuation_tax.setdefault(tax.name, 0) @@ -871,8 +877,19 @@ class PurchaseInvoice(BuyingController): "against": self.supplier, "credit": valuation_tax[tax.name], "remarks": self.remarks or "Accounting Entry for Stock" - }, item=tax) - ) + }, item=tax)) + + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.supplier, + "credit": flt(self.total_taxes_and_charges), + "credit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries @@ -1032,7 +1049,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 @@ -1088,7 +1107,9 @@ class PurchaseInvoice(BuyingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate: + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 86c2e408c0b..8da7d6fe13d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -4,23 +4,25 @@ // render frappe.listview_settings['Purchase Invoice'] = { add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company", - "currency", "is_return", "release_date", "on_hold"], + "currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"], get_indicator: function(doc) { - if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { + if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"]; - } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { + } else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { if(cint(doc.on_hold) && !doc.release_date) { return [__("On Hold"), "darkgrey"]; - } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { + } else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { return [__("Temporarily on Hold"), "darkgrey"]; - } else if(frappe.datetime.get_diff(doc.due_date) < 0) { + } else if (frappe.datetime.get_diff(doc.due_date) < 0) { return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"]; } else { return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"]; } - } else if(cint(doc.is_return)) { + } else if (cint(doc.is_return)) { return [__("Return"), "darkgrey", "is_return,=,Yes"]; - } else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) { + } else if (doc.company == doc.represents_company && doc.is_internal_supplier) { + return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"]; + } else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) { return [__("Paid"), "green", "outstanding_amount,=,0"]; } } diff --git a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json index 23dc6c47e8d..f1ed8efa319 100644 --- a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json +++ b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json @@ -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" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 502e65ed8d0..5efc32e11d9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 17fbe2def9d..6799fb986aa 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", @@ -158,6 +157,7 @@ "more_information", "inter_company_invoice_reference", "is_internal_customer", + "represents_company", "customer_group", "campaign", "is_discounted", @@ -171,6 +171,7 @@ "c_form_applicable", "c_form_no", "column_break8", + "unrealized_profit_loss_account", "remarks", "sales_team_section_break", "sales_partner", @@ -1655,7 +1656,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, "read_only": 1 }, @@ -1950,13 +1951,31 @@ "fieldtype": "Data", "label": "Company Tax ID", "read_only": 1 + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Company which internal customer represents", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:45.086303", + "modified": "2020-12-11 12:48:31.769958", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index af6c6968dc1..ca6f22cc30b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -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) @@ -751,6 +758,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) @@ -770,7 +778,7 @@ class SalesInvoice(SellingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: + if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) @@ -809,6 +817,18 @@ class SalesInvoice(SellingController): }, account_currency, item=tax) ) + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.customer, + "debit": flt(self.total_taxes_and_charges), + "debit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) + def make_item_gl_entries(self, gl_entries): # income account gl entries for item in self.get("items"): @@ -831,22 +851,24 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", self.posting_date) asset.set_status("Sold" if self.docstatus==1 else None) else: - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + # Do not book income for transfer within same company + if not self.is_internal_transfer(): + income_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) - account_currency = get_account_currency(income_account) - gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(item.base_net_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) - if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + account_currency = get_account_currency(income_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_account, + "against": self.customer, + "credit": flt(item.base_net_amount, item.precision("base_net_amount")), + "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) + if account_currency==self.company_currency + else flt(item.net_amount, item.precision("net_amount"))), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # expense account gl entries if cint(self.update_stock) and \ @@ -1258,7 +1280,9 @@ class SalesInvoice(SellingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': self.status = "Overdue and Discounted" elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" @@ -1523,9 +1547,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + source_document_warehouse_field = 'target_warehouse' + target_document_warehouse_field = 'from_warehouse' else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" + source_document_warehouse_field = 'from_warehouse' + target_document_warehouse_field = 'target_warehouse' validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -1552,6 +1580,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if currency: target_doc.currency = currency + item_field_map = { + "doctype": target_doctype + " Item", + "field_no_map": [ + "income_account", + "expense_account", + "cost_center", + "warehouse" + ] + } + + if source_doc.get('update_stock'): + item_field_map.update({ + 'field_map': { + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + } + }) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, @@ -1560,15 +1608,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "taxes_and_charges" ] }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_no_map": [ - "income_account", - "expense_account", - "cost_center", - "warehouse" - ] - } + doctype +" Item": item_field_map }, target_doc, set_missing_values) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 05d49df711a..41140d19388 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = { "Credit Note Issued": "darkgrey", "Unpaid and Discounted": "orange", "Overdue and Discounted": "red", - "Overdue": "red" - + "Overdue": "red", + "Internal Transfer": "darkgrey" }; return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9660c9570e2..22a4f336547 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -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") @@ -1571,7 +1573,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1605,9 +1607,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) @@ -1779,6 +1781,60 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) + + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) + + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() + + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], + ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] + ] + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + def test_eway_bill_json(self): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ @@ -2037,4 +2093,57 @@ def get_taxes_and_charges(): "parentfield": "taxes", "rate": 2, "row_id": 1 - }] \ No newline at end of file + }] + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company + }) + + customer.append("companies", { + "company": allowed_to_interact_with + }) + + customer.insert() + customer_name = customer.name + else: + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name + +def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company + }) + + supplier.append("companies", { + "company": allowed_to_interact_with + }) + + supplier.insert() + supplier_name = supplier.name + else: + supplier_name = frappe.db.exists("Supplier", supplier_name) + + return supplier_name + +def add_taxes(doc): + doc.append('taxes', { + 'account_head': '_Test Account Excise Duty - TCP1', + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12 + }) \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index bb0d0a132a5..79a6aabd987 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -42,11 +42,13 @@ {% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); - var range1 = report.columns[11].label; - var range2 = report.columns[12].label; - var range3 = report.columns[13].label; - var range4 = report.columns[14].label; - var range5 = report.columns[15].label; + var start = filters.based_on_payment_terms ? 13 : 11; + var range1 = report.columns[start].label; + var range2 = report.columns[start+1].label; + var range3 = report.columns[start+2].label; + var range4 = report.columns[start+3].label; + var range5 = report.columns[start+4].label; + var range6 = report.columns[start+5].label; %} {% if(balance_row) { %} @@ -70,20 +72,34 @@ + - - - - - + + + + + + + @@ -91,6 +107,7 @@ + @@ -101,6 +118,7 @@ + @@ -218,15 +236,15 @@ + {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %} {% if(!filters.show_future_payments) { %} - + {%= format_currency(data[i]["paid"], data[i]["currency"]) %} + {% } %} + {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %} {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} @@ -234,8 +252,8 @@ {%= data[i]["po_no"] %} {% } %} - - + + {% } %} {% } %} {% } else { %} @@ -256,10 +274,10 @@ {% } else { %} {% } %} - - - - + + + + {% } %} {% } %} diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 3ffb3ac1df4..515fd995e66 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -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(): diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 3445df7206f..a36e7f8581f 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -8,6 +8,7 @@ from frappe.utils import flt from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (get_tax_accounts, get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row, get_group_by_conditions) +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details def execute(filters=None): return _execute(filters) @@ -22,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum aii_account_map = get_aii_accounts() if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency, - doctype="Purchase Invoice", tax_doctype="Purchase Taxes and Charges") + doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges') po_pr_map = get_purchase_receipts_against_purchase_order(item_list) @@ -34,10 +35,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Purchase Invoice') + item_details = get_item_details() + for d in item_list: if not d.stock_qty: continue + item_record = item_details.get(d.item_code) + purchase_receipt = None if d.purchase_receipt: purchase_receipt = d.purchase_receipt @@ -48,8 +53,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, @@ -81,10 +86,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), + frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), }) - total_tax += flt(item_tax.get("tax_amount")) + total_tax += flt(item_tax.get('tax_amount')) row.update({ 'total_tax': total_tax, @@ -309,8 +314,8 @@ def get_items(filters, additional_query_columns): select `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, - `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice Item`.`item_code`, - `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.description, + `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index a05dcd75ce5..f54ceb0d2f5 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -8,6 +8,7 @@ from frappe.utils import flt, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details, get_customer_details def execute(filters=None): return _execute(filters) @@ -16,7 +17,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if not filters: filters = {} columns = get_columns(additional_table_columns, filters) - company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') item_list = get_items(filters, additional_query_columns) if item_list: @@ -33,7 +34,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get('group_by'): grand_total = get_grand_total(filters, 'Sales Invoice') + customer_details = get_customer_details() + item_details = get_item_details() + for d in item_list: + customer_record = customer_details.get(d.customer) + item_record = item_details.get(d.item_code) + delivery_note = None if d.delivery_note: delivery_note = d.delivery_note @@ -45,14 +52,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { 'item_code': d.item_code, - 'item_name': d.item_name, - 'item_group': d.item_group, + 'item_name': item_record.item_name, + 'item_group': item_record.item_group, 'description': d.description, 'invoice': d.parent, 'posting_date': d.posting_date, 'customer': d.customer, - 'customer_name': d.customer_name, - 'customer_group': d.customer_group, + 'customer_name': customer_record.customer_name, + 'customer_group': customer_record.customer_group, } if additional_query_columns: @@ -90,10 +97,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0), - frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0), + frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), + frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), }) - total_tax += flt(item_tax.get("tax_amount")) + total_tax += flt(item_tax.get('tax_amount')) row.update({ 'total_tax': total_tax, @@ -226,7 +233,7 @@ def get_columns(additional_table_columns, filters): if filters.get('group_by') != 'Territory': columns.extend([ { - 'label': _("Territory"), + 'label': _('Territory'), 'fieldname': 'territory', 'fieldtype': 'Link', 'options': 'Territory', @@ -374,13 +381,12 @@ def get_items(filters, additional_query_columns): `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, - `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, - `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.description, `tabSales Invoice Item`.sales_order, - `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, - `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, - `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, - `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, - `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, + `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, + `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, + `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, + `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} from `tabSales Invoice`, `tabSales Invoice Item` where `tabSales Invoice`.name = `tabSales Invoice Item`.parent @@ -417,14 +423,14 @@ def get_deducted_taxes(): return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'") def get_tax_accounts(item_list, columns, company_currency, - doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): + doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'): import json item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), + tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'), currency=company_currency) or 2 for d in item_list: @@ -469,8 +475,8 @@ def get_tax_accounts(item_list, columns, company_currency, tax_rate = tax_data tax_amount = 0 - if charge_type == "Actual" and not tax_rate: - tax_rate = "NA" + if charge_type == 'Actual' and not tax_rate: + tax_rate = 'NA' item_net_amount = sum([flt(d.base_net_amount) for d in item_row_map.get(parent, {}).get(item_code, [])]) @@ -484,17 +490,17 @@ def get_tax_accounts(item_list, columns, company_currency, if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": tax_rate, - "tax_amount": tax_value + 'tax_rate': tax_rate, + 'tax_amount': tax_value }) except ValueError: continue - elif charge_type == "Actual" and tax_amount: + elif charge_type == 'Actual' and tax_amount: for d in invoice_item_row.get(parent, []): itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - "tax_rate": "NA", - "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, + 'tax_rate': 'NA', + 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision) }) @@ -563,7 +569,7 @@ def add_total_row(data, filters, prev_group_by_value, item, total_row_map, }) total_row_map.setdefault('total_row', { - subtotal_display_field: "Total", + subtotal_display_field: 'Total', 'stock_qty': 0.0, 'amount': 0.0, 'bold': 1, diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index a9e25bc25bf..2e18ce11ddc 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -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, diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index 5e8d7730b76..e9e9c9c4e69 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -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(): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 53677cde8aa..550aaef4040 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -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) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index fd702c74c73..74ca62ffdad 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -13,8 +13,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g class AssetValueAdjustment(Document): def validate(self): self.validate_date() - self.set_difference_amount() self.set_current_asset_value() + self.set_difference_amount() def on_submit(self): self.make_depreciation_entry() @@ -25,7 +25,7 @@ class AssetValueAdjustment(Document): frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) self.reschedule_depreciations(self.current_asset_value) - + def validate_date(self): asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date') if getdate(self.date) < getdate(asset_purchase_date): @@ -53,6 +53,7 @@ class AssetValueAdjustment(Document): je.posting_date = self.date je.company = self.company je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) + je.finance_book = self.finance_book credit_entry = { "account": accumulated_depreciation_account, @@ -78,7 +79,7 @@ class AssetValueAdjustment(Document): debit_entry.update({ dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') }) - + je.append("accounts", credit_entry) je.append("accounts", debit_entry) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index af08a2a601e..d1457b9b85a 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -75,24 +75,23 @@ def get_data(filters): for asset in assets_record: asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ - flt(depreciation_amount_map.get(asset.name)) - if asset_value: - row = { - "asset_id": asset.asset_id, - "asset_name": asset.asset_name, - "status": asset.status, - "department": asset.department, - "cost_center": asset.cost_center, - "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), - "gross_purchase_amount": asset.gross_purchase_amount, - "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, - "available_for_use_date": asset.available_for_use_date, - "location": asset.location, - "asset_category": asset.asset_category, - "purchase_date": asset.purchase_date, - "asset_value": asset_value - } - data.append(row) + row = { + "asset_id": asset.asset_id, + "asset_name": asset.asset_name, + "status": asset.status, + "department": asset.department, + "cost_center": asset.cost_center, + "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), + "gross_purchase_amount": asset.gross_purchase_amount, + "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, + "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "available_for_use_date": asset.available_for_use_date, + "location": asset.location, + "asset_category": asset.asset_category, + "purchase_date": asset.purchase_date, + "asset_value": asset_value + } + data.append(row) return data diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 71231f68c84..75da71ceff8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -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", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index c4272422081..e537771eaf2 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -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")); } }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 3af6cf8e5d9..4ce4100a7fc 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -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", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index df143eefa0d..0ee9d180d99 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -49,6 +49,12 @@ class Supplier(TransactionBase): msgprint(_("Series is mandatory"), raise_exception=1) validate_party_accounts(self) + self.validate_internal_supplier() + + def validate_internal_supplier(self): + if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Supplier for company {0} already exists").format( + frappe.bold(self.represents_company))) def on_trash(self): delete_contact_and_address('Supplier', self.name) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index a7cab5015e3..a3b2085400e 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -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) { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index b39c9890735..40fbe2c26e5 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -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", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6108a614ca5..32c5d3a3b14 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -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): @@ -105,6 +107,8 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.set_inter_company_account() + validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) @@ -735,6 +739,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 += "
" + _("Please set one of the following:") + "
" + message += "
" + + 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) @@ -915,6 +934,38 @@ class AccountsController(TransactionBase): else: return frappe.db.get_single_value("Global Defaults", "disable_rounded_total") + def set_inter_company_account(self): + """ + Set intercompany account for inter warehouse transactions + This account will be used in case billing company and internal customer's + representation company is same + """ + + if self.is_internal_transfer() and not self.unrealized_profit_loss_account: + unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account') + + if not unrealized_profit_loss_account: + msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format( + frappe.bold(self.company)) + frappe.throw(msg) + + self.unrealized_profit_loss_account = unrealized_profit_loss_account + + def is_internal_transfer(self): + """ + It will an internal transfer if its an internal customer and representation + company is same as billing company + """ + if self.doctype == 'Sales Invoice': + internal_party_field = 'is_internal_customer' + else: + internal_party_field = 'is_internal_supplier' + + if self.get(internal_party_field) and (self.represents_company == self.company): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9ee83e34816..286c4f44510 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -42,6 +42,7 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() + self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -94,13 +95,23 @@ class BuyingController(StockController): def validate_stock_or_nonstock_items(self): if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items(): - tax_for_valuation = [d for d in self.get("taxes") + msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') + self.update_tax_category(msg) + + def update_tax_category_for_internal_transfer(self): + if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): + msg = _('Tax Category has been changed to "Total" as its an internal purchase.') + self.update_tax_category(msg) + + def update_tax_category(self, msg): + tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] - if tax_for_valuation: - for d in tax_for_valuation: - d.category = 'Total' - msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) + if tax_for_valuation: + for d in tax_for_valuation: + d.category = 'Total' + + msgprint(msg) def validate_asset_return(self): if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: @@ -497,6 +508,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: diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index afc5f8179f5..5299b25601d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -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 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 515239a982f..4dbd7bfa186 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -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(',')))) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 9feac787709..8c05134ae41 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -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): @@ -252,33 +254,43 @@ class StatusUpdater(Document): if not args.get("second_source_extra_cond"): args["second_source_extra_cond"] = "" - args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) + args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args + and (`tab%(second_source_dt)s`.docstatus=1) + %(second_source_extra_cond)s), 0) """ % args)[0][0] if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" - frappe.db.sql("""update `tab%(target_dt)s` - set %(target_field)s = ( + args["source_dt_value"] = frappe.db.sql(""" (select ifnull(sum(%(source_field)s), 0) from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s" and (docstatus=1 %(cond)s) %(extra_cond)s) - %(second_source_condition)s - ) - %(update_modified)s + """ % args)[0][0] or 0.0 + + if args['second_source_condition']: + args["source_dt_value"] += flt(args['second_source_condition']) + + frappe.db.sql("""update `tab%(target_dt)s` + set %(target_field)s = %(source_dt_value)s %(update_modified)s where name='%(detail_id)s'""" % args) 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""" diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2d2fff8fd54..683d7f77b55 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -77,7 +77,7 @@ class StockController(AccountsController): if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): - # from warehouse account/ target warehouse account + # from warehouse account self.check_expense_account(item_row) @@ -92,9 +92,16 @@ class StockController(AccountsController): sle = self.update_stock_ledger_entries(sle) + # expense account/ target_warehouse / source_warehouse + if item_row.get('target_warehouse'): + warehouse = item_row.get('target_warehouse') + expense_account = warehouse_account[warehouse]["account"] + else: + expense_account = item_row.expense_account + gl_list.append(self.get_gl_dict({ "account": warehouse_account[sle.warehouse]["account"], - "against": item_row.expense_account, + "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), "remarks": self.get("remarks") or "Accounting Entry for Stock", @@ -102,9 +109,8 @@ class StockController(AccountsController): "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - # expense account gl_list.append(self.get_gl_dict({ - "account": item_row.expense_account, + "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), @@ -340,11 +346,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) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 81d07c1327e..8dd2e5bacbd 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -519,6 +519,17 @@ class calculate_taxes_and_totals(object): if self.doc.docstatus == 0: self.calculate_outstanding_amount() + def is_internal_invoice(self): + """ + Checks if its an internal transfer invoice + and decides if to calculate any out standing amount or not + """ + + if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer(): + return True + + return False + def calculate_outstanding_amount(self): # NOTE: # write_off_amount is only for POS Invoice @@ -526,7 +537,8 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \ + self.is_internal_invoice(): return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self._set_in_company_currency(self.doc, ['write_off_amount']) @@ -641,7 +653,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 diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index ee9e8951301..99688551630 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -1,23 +1,31 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -cur_frm.add_fetch("contract_template", "contract_terms", "contract_terms"); -cur_frm.add_fetch("contract_template", "requires_fulfilment", "requires_fulfilment"); - -// Add fulfilment terms from contract template into contract frappe.ui.form.on("Contract", { contract_template: function (frm) { - // Populate the fulfilment terms table from a contract template, if any if (frm.doc.contract_template) { - frappe.model.with_doc("Contract Template", frm.doc.contract_template, function () { - var tabletransfer = frappe.model.get_doc("Contract Template", frm.doc.contract_template); - - frm.doc.fulfilment_terms = []; - $.each(tabletransfer.fulfilment_terms, function (index, row) { - var d = frm.add_child("fulfilment_terms"); - d.requirement = row.requirement; - frm.refresh_field("fulfilment_terms"); - }); + frappe.call({ + method: 'erpnext.crm.doctype.contract_template.contract_template.get_contract_template', + args: { + template_name: frm.doc.contract_template, + doc: frm.doc + }, + callback: function(r) { + if (r && r.message) { + let contract_template = r.message.contract_template; + frm.set_value("contract_terms", r.message.contract_terms); + frm.set_value("requires_fulfilment", contract_template.requires_fulfilment); + + if (frm.doc.requires_fulfilment) { + // Populate the fulfilment terms table from a contract template, if any + r.message.contract_template.fulfilment_terms.forEach(element => { + let d = frm.add_child("fulfilment_terms"); + d.requirement = element.requirement; + }); + frm.refresh_field("fulfilment_terms"); + } + } + } }); } } diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 0026e4a02eb..de3230f0e67 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2018-04-12 06:32:04.582486", @@ -247,7 +248,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-30 06:56:07.257932", + "modified": "2020-12-07 11:15:58.385521", "modified_by": "Administrator", "module": "CRM", "name": "Contract", diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index 5e4582f8d3d..7cc5ec13cf7 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -11,7 +11,9 @@ "contract_terms", "sb_fulfilment", "requires_fulfilment", - "fulfilment_terms" + "fulfilment_terms", + "section_break_6", + "contract_template_help" ], "fields": [ { @@ -41,10 +43,20 @@ "fieldtype": "Table", "label": "Fulfilment Terms and Conditions", "options": "Contract Template Fulfilment Terms" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "contract_template_help", + "fieldtype": "HTML", + "label": "Contract Template Help", + "options": "

Contract Template Example

\n\n
Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n
\n\n

How to get fieldnames

\n\n

The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

" } ], "links": [], - "modified": "2020-11-11 17:49:44.879363", + "modified": "2020-12-07 10:44:22.587047", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", diff --git a/erpnext/crm/doctype/contract_template/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py index 601ee9a28b3..69fd86f7fb5 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.py +++ b/erpnext/crm/doctype/contract_template/contract_template.py @@ -5,6 +5,27 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils.jinja import validate_template +from six import string_types +import json class ContractTemplate(Document): - pass + def validate(self): + if self.contract_terms: + validate_template(self.contract_terms) + +@frappe.whitelist() +def get_contract_template(template_name, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + contract_template = frappe.get_doc("Contract Template", template_name) + contract_terms = None + + if contract_template.contract_terms: + contract_terms = frappe.render_template(contract_template.contract_terms, doc) + + return { + 'contract_template': contract_template, + 'contract_terms': contract_terms + } \ No newline at end of file diff --git a/erpnext/demo/setup/setup_data.py b/erpnext/demo/setup/setup_data.py index a395c7c17ae..05ee28a24a4 100644 --- a/erpnext/demo/setup/setup_data.py +++ b/erpnext/demo/setup/setup_data.py @@ -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() diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index efbaa71924e..f0a05ed192f 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -260,6 +260,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): """Shipping lines represents the shipping details, each such shipping detail consists of a list of tax_lines""" for shipping_charge in shipping_lines: + if shipping_charge.get("price"): + taxes.append({ + "charge_type": _("Actual"), + "account_head": get_tax_account_head(shipping_charge), + "description": shipping_charge["title"], + "tax_amount": shipping_charge["price"], + "cost_center": shopify_settings.cost_center + }) + for tax in shipping_charge.get("tax_lines"): taxes.append({ "charge_type": _("Actual"), diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 24fc3d44b99..f960998c3c9 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -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: diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index e278fd78071..362f6cf88ee 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -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) \ No newline at end of file + 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 diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 81d60481ce6..af601f3eb2e 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -30,6 +30,11 @@ "label": "Laboratory", "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test\",\n\t\t\"label\": \"Lab Test\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sample Collection\",\n\t\t\"label\": \"Sample Collection\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Dosage Form\",\n\t\t\"label\": \"Dosage Form\"\n\t}\n]" }, + { + "hidden": 0, + "label": "Inpatient", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Order\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Entry\",\n\t\t\"label\": \"Inpatient Medication Entry\"\n\t}\n]" + }, { "hidden": 0, "label": "Rehabilitation and Physiotherapy", @@ -38,7 +43,7 @@ { "hidden": 0, "label": "Records and History", - "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]" + "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t}\n]" }, { "hidden": 0, @@ -64,7 +69,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-11-23 23:00:48.764377", + "modified": "2020-11-26 22:09:09.164584", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index eb7d4bdebad..1d4411d73de 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', { callback: function(r) { if (r.message) { frappe.show_alert({ - message: __('Stock Entry {0} created', - ['' + r.message + '']), + message: __('Stock Entry {0} created', ['' + r.message + '']), 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() { diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index cdf692e68bd..7e7fd824119 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -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() diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js index f523cf21bd4..ca97489b8d8 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js @@ -29,6 +29,29 @@ frappe.ui.form.on('Inpatient Medication Entry', { } }; }); + + if (frm.doc.__islocal || frm.doc.docstatus !== 0 || !frm.doc.update_stock) + return; + + frm.add_custom_button(__('Make Stock Entry'), function() { + frappe.call({ + method: 'erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry.make_difference_stock_entry', + args: { docname: frm.doc.name }, + freeze: true, + callback: function(r) { + if (r.message) { + var doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } else { + frappe.msgprint({ + title: __('No Drug Shortage'), + message: __('All the drugs are available with sufficient qty to process this Inpatient Medication Entry.'), + indicator: 'green' + }); + } + } + }); + }); }, patient: function(frm) { diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index 5dac23abd90..70ae7138662 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -142,25 +142,32 @@ class InpatientMedicationEntry(Document): return orders, order_entry_map def check_stock_qty(self): - from erpnext.stock.stock_ledger import NegativeStockError + drug_shortage = get_drug_shortage_map(self.medication_orders, self.warehouse) - drug_availability = dict() - for d in self.medication_orders: - if not drug_availability.get(d.drug_code): - drug_availability[d.drug_code] = 0 - drug_availability[d.drug_code] += flt(d.dosage) + if drug_shortage: + message = _('Quantity not available for the following items in warehouse {0}. ').format(frappe.bold(self.warehouse)) + message += _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.') - for drug, dosage in drug_availability.items(): - available_qty = get_latest_stock_qty(drug, self.warehouse) + formatted_item_rows = '' - # validate qty - if flt(available_qty) < flt(dosage): - frappe.throw(_('Quantity not available for {0} in warehouse {1}').format( - frappe.bold(drug), frappe.bold(self.warehouse)) - + '

' + _('Available quantity is {0}, you need {1}').format( - frappe.bold(available_qty), frappe.bold(dosage)) - + '

' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'), - NegativeStockError, title=_('Insufficient Stock')) + for drug, shortage_qty in drug_shortage.items(): + item_link = get_link_to_form('Item', drug) + formatted_item_rows += """ + + + """.format(item_link, frappe.bold(shortage_qty)) + + message += """ +
{%= __(range3) %} {%= __(range4) %} {%= __(range5) %}{%= __(range6) %} {%= __("Total") %}
{%= __("Total Outstanding") %}{%= format_number(balance_row["range1"], null, 2) %}{%= format_currency(balance_row["range2"]) %}{%= format_currency(balance_row["range3"]) %}{%= format_currency(balance_row["range4"]) %}{%= format_currency(balance_row["range5"]) %} + {%= format_number(balance_row["age"], null, 2) %} + + {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %} + {%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %} -
{%= __("Future Payments") %} {%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
{%= __("Total") %} - {%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %} - {%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} {%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} - {%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= data[i]["future_ref"] %}{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}{%= __("Total") %}{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}{%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}{%= format_currency(data[i]["paid"], data[i]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}
{0}{1}
+ + + + + {2} +
{0}{1}
+ """.format(_('Drug Code'), _('Shortage Qty'), formatted_item_rows) + + frappe.throw(message, title=_('Insufficient Stock'), is_minimizable=True, wide=True) def make_stock_entry(self): stock_entry = frappe.new_doc('Stock Entry') @@ -223,7 +230,8 @@ def get_pending_medication_orders(entry): for doc in data: inpatient_record = doc.inpatient_record - doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record) + if inpatient_record: + doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record) if entry.service_unit and doc.service_unit != entry.service_unit: to_remove.append(doc) @@ -276,4 +284,55 @@ def get_current_healthcare_service_unit(inpatient_record): ip_record = frappe.get_doc('Inpatient Record', inpatient_record) if ip_record.inpatient_occupancies: return ip_record.inpatient_occupancies[-1].service_unit - return \ No newline at end of file + return + + +def get_drug_shortage_map(medication_orders, warehouse): + """ + Returns a dict like { drug_code: shortage_qty } + """ + drug_requirement = dict() + for d in medication_orders: + if not drug_requirement.get(d.drug_code): + drug_requirement[d.drug_code] = 0 + drug_requirement[d.drug_code] += flt(d.dosage) + + drug_shortage = dict() + for drug, required_qty in drug_requirement.items(): + available_qty = get_latest_stock_qty(drug, warehouse) + if flt(required_qty) > flt(available_qty): + drug_shortage[drug] = flt(flt(required_qty) - flt(available_qty)) + + return drug_shortage + + +@frappe.whitelist() +def make_difference_stock_entry(docname): + doc = frappe.get_doc('Inpatient Medication Entry', docname) + drug_shortage = get_drug_shortage_map(doc.medication_orders, doc.warehouse) + + if not drug_shortage: + return None + + stock_entry = frappe.new_doc('Stock Entry') + stock_entry.purpose = 'Material Transfer' + stock_entry.set_stock_entry_type() + stock_entry.to_warehouse = doc.warehouse + stock_entry.company = doc.company + cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center') + expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company) + + for drug, shortage_qty in drug_shortage.items(): + se_child = stock_entry.append('items') + se_child.item_code = drug + se_child.item_name = frappe.db.get_value('Item', drug, 'stock_uom') + se_child.uom = frappe.db.get_value('Item', drug, 'stock_uom') + se_child.stock_uom = se_child.uom + se_child.qty = flt(shortage_qty) + se_child.t_warehouse = doc.warehouse + # in stock uom + se_child.conversion_factor = 1 + se_child.cost_center = cost_center + se_child.expense_account = expense_account + + return stock_entry diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py index 2f1bb6b56ff..7cb5a4814e8 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -9,6 +9,7 @@ from frappe.utils import add_days, 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.doctype.inpatient_medication_entry.inpatient_medication_entry import get_drug_shortage_map, make_difference_stock_entry from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account class TestInpatientMedicationEntry(unittest.TestCase): @@ -82,6 +83,39 @@ class TestInpatientMedicationEntry(unittest.TestCase): self.assertEqual(stock_entry.items[0].patient, self.patient) self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name) + def test_drug_shortage_stock_entry(self): + ipmo = create_ipmo(self.patient) + ipmo.submit() + ipmo.reload() + + date = add_days(getdate(), -1) + filters = frappe._dict( + from_date=date, + to_date=date, + from_time='', + to_time='', + item_code='Dextromethorphan', + patient=self.patient + ) + + # check drug shortage + ipme = create_ipme(filters, update_stock=1) + ipme.warehouse = 'Finished Goods - _TC' + ipme.save() + drug_shortage = get_drug_shortage_map(ipme.medication_orders, ipme.warehouse) + self.assertEqual(drug_shortage.get('Dextromethorphan'), 3) + + # check material transfer for drug shortage + make_stock_entry() + stock_entry = make_difference_stock_entry(ipme.name) + self.assertEqual(stock_entry.items[0].item_code, 'Dextromethorphan') + self.assertEqual(stock_entry.items[0].qty, 3) + stock_entry.from_warehouse = 'Stores - _TC' + stock_entry.submit() + + ipme.reload() + ipme.submit() + def tearDown(self): # cleanup - Discharge schedule_discharge(frappe.as_json({'patient': self.patient})) @@ -94,15 +128,12 @@ class TestInpatientMedicationEntry(unittest.TestCase): for entry in frappe.get_all('Inpatient Medication Entry'): doc = frappe.get_doc('Inpatient Medication Entry', entry.name) doc.cancel() - frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name}) - 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 make_stock_entry(): +def make_stock_entry(warehouse=None): frappe.db.set_value('Company', '_Test Company', { 'stock_adjustment_account': 'Stock Adjustment - _TC', 'default_inventory_account': 'Stock In Hand - _TC' @@ -110,7 +141,7 @@ def make_stock_entry(): stock_entry = frappe.new_doc('Stock Entry') stock_entry.stock_entry_type = 'Material Receipt' stock_entry.company = '_Test Company' - stock_entry.to_warehouse = 'Stores - _TC' + stock_entry.to_warehouse = warehouse or 'Stores - _TC' expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company') se_child = stock_entry.append('items') se_child.item_code = 'Dextromethorphan' diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py index e3def72334c..39603f77a06 100644 --- a/erpnext/healthcare/doctype/patient/patient_dashboard.py +++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py @@ -18,6 +18,10 @@ def get_data(): { 'label': _('Billing'), 'items': ['Sales Invoice'] + }, + { + 'label': _('Orders'), + 'items': ['Inpatient Medication Order'] } ] } diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index eeed157291e..3df7ba15314 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -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() diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index aa85a231132..419d956425e 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -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() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f4..1e3bb6a5cfb 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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}, diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index da789198e51..4f1c04ff5d0 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -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", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index cba8ee9a404..5037ceb489e 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -15,11 +15,21 @@ frappe.ui.form.on('Employee Advance', { }); frm.set_query("advance_account", function() { + if (!frm.doc.employee) { + frappe.msgprint(__("Please select employee first")); + } + let company_currency = erpnext.get_currency(frm.doc.company); + let currencies = [company_currency]; + if (frm.doc.currency && (frm.doc.currency != company_currency)) { + currencies.push(frm.doc.currency); + } + return { filters: { "root_type": "Asset", "is_group": 0, - "company": frm.doc.company + "company": frm.doc.company, + "account_currency": ["in", currencies], } }; }); @@ -63,7 +73,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 +137,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 +150,74 @@ 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) { + if (frm.doc.currency) { + 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); + } + }); } }); diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 0d909138719..cf6b5404ecf 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -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", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 3c435b8cc3b..cb72f6b6d96 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -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 \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 2097e711de4..c88b2b8e49e 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -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" diff --git a/erpnext/hr/doctype/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json index e63ffae0c42..88b061a3c3b 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade.json +++ b/erpnext/hr/doctype/employee_grade/employee_grade.json @@ -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 } \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 6e97f0513d6..4a0908d457e 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -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", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index 885e3eed976..020457d4ec6 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -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", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 4374d2911aa..f99963504ab 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -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", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 007497e34a5..4b315014dae 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -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", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 03fe3fa035c..a09cd2ea112 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -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")) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 3f25f583833..4f3e4623909 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -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) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 6e909c3f01b..53b7a39e511 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -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 diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js index 71a34226da4..81936a4a383 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js @@ -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(); + } + } + }); + }, }); diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index 2cf6ccf5ca0..83eeae3adba 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -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", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index c1dcc97b1a9..4c1a46522f6 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -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)) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99f64634161..aafc9642d46 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -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() diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js index bad2b8766c8..0e88bc16714 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.js +++ b/erpnext/hr/doctype/leave_period/leave_period.js @@ -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(); - } + }, }); diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 0973ac71985..28a33f6fac8 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -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 \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 1762cf917a2..b5857bcd8fe 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -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', diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py similarity index 100% rename from erpnext/communication/doctype/call_log/__init__.py rename to erpnext/hr/doctype/leave_policy_assignment/__init__.py diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js new file mode 100644 index 00000000000..7c32a0dde09 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js @@ -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 += "
"; + msg += ""; + for (let key in leave_allocations) { + msg += ""; + } + msg += "
"+__('Leave Type')+""+__("Leave Allocation")+""+__("Leaves Granted")+"
"+key+""+leave_allocations[key]["name"]+""+leave_allocations[key]["leaves"]+"
"; + 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(); + } + +}); diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json new file mode 100644 index 00000000000..ecebb3b7d6c --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py new file mode 100644 index 00000000000..a5068bc26d8 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -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 + diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js new file mode 100644 index 00000000000..468f243885c --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -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); + }); + } + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py new file mode 100644 index 00000000000..c7bc6fb7755 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -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 + + diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 0af832f903e..a2092919f8f 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -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", diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index c0d12968416..21f180b857d 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -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")) diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index 0c4f435860a..7fef2975c8a 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -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 \ No newline at end of file diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 8d95924681a..d700e7fccf2 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -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() diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index d468f52bc0f..acf09f5c037 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -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", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 8405d6ec62b..cd40a665d43 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -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)) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 10a7b1143d1..a63d06590f8 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -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) diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index 687c58000e2..2a659e9fc2e 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -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}) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8888a967683..6363242b0a6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -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 diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e53927918eb..2bf3fbf75e9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -491,6 +491,39 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_partial_material_consumption(self): + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) + + ste_cancel_list = [] + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + + ste_cancel_list.extend([ste1, ste2]) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4)) + s.submit() + ste_cancel_list.append(s) + + ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2)) + ste1.submit() + ste_cancel_list.append(ste1) + + print(wo_order.name) + ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) + self.assertEquals(ste3.fg_completed_qty, 2) + + expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4} + for row in ste3.items: + self.assertEquals(row.qty, expected_qty.get(row.item_code)) + + for ste_doc in ste_cancel_list: + ste_doc.cancel() + + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9ce465ccaf7..a6086fb88da 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -545,7 +545,8 @@ erpnext.work_order = { var tbl = frm.doc.required_items || []; var tbl_lenght = tbl.length; for (var i = 0, len = tbl_lenght; i < len; i++) { - if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) { + let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { counter += 1; } } diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index ebc01c65afb..806d268ffde 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -124,7 +124,7 @@ class ProductionPlanReport(object): if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)") raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code, - bom_item.item_name as raw_material_name, {0} as required_qty + bom_item.item_name as raw_material_name, {0} as required_qty_per_unit FROM `tabBOM` as bom, `tab{1}` as bom_item WHERE @@ -208,7 +208,7 @@ class ProductionPlanReport(object): warehouses = self.mrp_warehouses or [] for d in self.raw_materials_dict.get(key): if self.filters.based_on != "Work Order": - d.required_qty = d.required_qty * data.qty_to_manufacture + d.required_qty = d.required_qty_per_unit * data.qty_to_manufacture if not warehouses: warehouses = [data.warehouse] diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 1e2aeea36a8..62f5dce8460 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -25,4 +25,5 @@ Hub Node Quality Management Communication Loan Management -Payroll \ No newline at end of file +Payroll +Telephony \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 44b975e9e9d..25d6b538300 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -59,7 +59,7 @@ class Member(Document): frappe.msgprint(_("A customer is already linked to this Member")) cust = create_customer(frappe._dict({ 'fullname': self.member_name, - 'email': self.email_id or self.user, + 'email': self.email_id or self.email, 'phone': None })) @@ -177,4 +177,4 @@ def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, m mobile=mobile )) - return member.name \ No newline at end of file + return member.name diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be8841174..86ac613ae5b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -691,6 +691,7 @@ erpnext.patches.v13_0.update_old_loans erpnext.patches.v12_0.set_serial_no_status #2020-05-21 erpnext.patches.v12_0.update_price_list_currency_in_bom execute:frappe.reload_doctype('Dashboard') +execute:frappe.reload_doc('desk', 'doctype', 'number_card_link') execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25 @@ -732,6 +733,11 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail +erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.updates_for_multi_currency_payroll +erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy +erpnext.patches.v13_0.add_po_to_global_search +erpnext.patches.v13_0.update_returned_qty_in_pr_dn diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index c51c38182cc..a908c16715a 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -8,8 +8,8 @@ from frappe.utils import getdate from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import DuplicateAssignment def execute(): - frappe.reload_doc('Payroll', 'doctype', 'salary_structure') - frappe.reload_doc("Payroll", "doctype", "salary_structure_assignment") + frappe.reload_doc('Payroll', 'doctype', 'Salary Structure') + frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment") frappe.db.sql(""" delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) @@ -33,6 +33,13 @@ def execute(): AND employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left') """.format(cols), as_dict=1) + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + + frappe.db.sql("""update `tabSalary Structure` set currency = %s where company=%s""", (company_currency, company)) + for d in ss_details: try: joining_date, relieving_date = frappe.db.get_value("Employee", d.employee, @@ -42,6 +49,7 @@ def execute(): from_date = joining_date elif relieving_date and getdate(from_date) > relieving_date: continue + company_currency = frappe.db.get_value('Company', d.company, 'default_currency') s = frappe.new_doc("Salary Structure Assignment") s.employee = d.employee @@ -52,6 +60,7 @@ def execute(): s.base = d.get("base") s.variable = d.get("variable") s.company = d.company + s.currency = company_currency # to migrate the data of the old employees s.flags.old_employee = True diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py new file mode 100644 index 00000000000..1c60b18e5b2 --- /dev/null +++ b/erpnext/patches/v13_0/add_po_to_global_search.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import frappe + + +def execute(): + global_search_settings = frappe.get_single("Global Search Settings") + + if "Purchase Order" in ( + dt.document_type for dt in global_search_settings.allowed_in_global_search + ): + return + + global_search_settings.append( + "allowed_in_global_search", {"document_type": "Purchase Order"} + ) + + global_search_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py new file mode 100644 index 00000000000..90dc0e2e18b --- /dev/null +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -0,0 +1,79 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + if "leave_policy" in frappe.db.get_table_columns("Employee"): + employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) + + employee_with_assignment = [] + leave_policy =[] + + #for employee + + for employee in employees_with_leave_policy: + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, employee.leave_policy) + + employee_with_assignment.append(employee.name) + leave_policy.append(employee.leave_policy) + + + if "default_leave_policy" in frappe.db.get_table_columns("Employee"): + employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) + + #for whole employee Grade + + for grade in employee_grade_with_leave_policy: + employees = get_employee_with_grade(grade.name) + for employee in employees: + + if employee not in employee_with_assignment: #Will ensure no duplicate + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, grade.default_leave_policy) + leave_policy.append(grade.default_leave_policy) + + #for old Leave allocation and leave policy from allocation, which may got updated in employee grade. + leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1) + + for allocation in leave_allocations: + if allocation.leave_policy not in leave_policy: + create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period, + allocation_exists=True) + +def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): + + filters = {"employee":employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period + + frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') + + if not frappe.db.exists("Leave Policy Assignment" , filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy + + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = 'Leave Period' + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 + + lpa.save() + if allocation_exists: + lpa.submit() + #Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + + +def get_employee_with_grade(grade): + return frappe.get_list("Employee", filters = {"grade": grade}) + + + diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index 96a63623c05..fa1dfed6435 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -29,7 +29,7 @@ def execute(): 'response_by_variance': response_by_variance, 'resolution_by_variance': resolution_by_variance, 'first_response_time': mins_to_first_response - }) + }, update_modified=False) # commit after every 100 updates count += 1 if count%100 == 0: @@ -44,7 +44,7 @@ def execute(): count = 0 for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response) + frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False) # commit after every 100 updates count += 1 if count%100 == 0: diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 77239429c51..561e967d6df 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -5,6 +5,8 @@ from frappe.utils import nowdate from erpnext.accounts.doctype.account.test_account import create_account from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.loan.loan import make_repayment_entry +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import get_accrued_interest_entries +from frappe.model.naming import make_autoname def execute(): @@ -18,15 +20,29 @@ def execute(): frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail') frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual') frappe.reload_doc('accounts', 'doctype', 'gl_entry') + frappe.reload_doc('accounts', 'doctype', 'journal_entry_account') updated_loan_types = [] + loans_to_close = [] + + # Update old loan status as closed + if frappe.db.has_column('Repayment Schedule', 'paid'): + loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule` + where paid = 0 and docstatus = 1""", as_dict=1) + + loans_to_close = [d.parent for d in loans_list] + + if loans_to_close: + frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close)) loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment', - 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account']) + 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'], + filters={'docstatus': 1, 'status': ('!=', 'Closed')}) for loan in loans: # Update details in Loan Types and Loan loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company') + loan_type = loan.loan_type group_income_account = frappe.get_value('Account', {'company': loan.company, 'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')}) @@ -38,7 +54,26 @@ def execute(): penalty_account = create_account(company=loan.company, account_type='Income Account', account_name='Penalty Account', parent_account=group_income_account) - if not loan_type_company: + # Same loan type used for multiple companies + if loan_type_company and loan_type_company != loan.company: + # get loan type for appropriate company + loan_type_name = frappe.get_value('Loan Type', {'company': loan.company, + 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account, + 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account, + 'penalty_income_account': loan.penalty_income_account}, 'name') + + if not loan_type_name: + loan_type_name = create_loan_type(loan, loan_type_name, penalty_account) + + # update loan type in loan + frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, + loan.name)) + + loan_type = loan_type_name + if loan_type_name not in updated_loan_types: + updated_loan_types.append(loan_type_name) + + elif not loan_type_company: loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type) loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company @@ -49,8 +84,9 @@ def execute(): loan_type_doc.penalty_income_account = penalty_account loan_type_doc.submit() updated_loan_types.append(loan.loan_type) + loan_type = loan.loan_type - if loan.loan_type in updated_loan_types: + if loan_type in updated_loan_types: if loan.status == 'Fully Disbursed': status = 'Disbursed' elif loan.status == 'Repaid/Closed': @@ -64,25 +100,48 @@ def execute(): 'status': status }) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type, + process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type, loan=loan.name) - payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date - FROM `tabJournal Entry` j, `tabJournal Entry Account` a - WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s - and account = %s - ''', (loan.name, loan.loan_account), as_dict=1) - for payment in payments: - repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant, - loan.loan_type, loan.company) + if frappe.db.has_column('Repayment Schedule', 'paid'): + total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name}, + ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest']) - repayment_entry.amount_paid = payment.debit_in_account_currency - repayment_entry.posting_date = payment.posting_date - repayment_entry.save() - repayment_entry.submit() + accrued_entries = get_accrued_interest_entries(loan.name) + for entry in accrued_entries: + interest_paid = 0 + principal_paid = 0 - jv = frappe.get_doc('Journal Entry', payment.name) - jv.flags.ignore_links = True - jv.cancel() + if total_interest > entry.interest_amount: + interest_paid = entry.interest_amount + else: + interest_paid = total_interest + if total_principal > entry.payable_principal_amount: + principal_paid = entry.payable_principal_amount + else: + principal_paid = total_principal + + frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + SET paid_principal_amount = `paid_principal_amount` + %s, + paid_interest_amount = `paid_interest_amount` + %s + WHERE name = %s""", + (principal_paid, interest_paid, entry.name)) + + total_principal -= principal_paid + total_interest -= interest_paid + +def create_loan_type(loan, loan_type_name, penalty_account): + loan_type_doc = frappe.new_doc('Loan Type') + loan_type_doc.loan_name = make_autoname("Loan Type-.####") + loan_type_doc.is_term_loan = 1 + loan_type_doc.company = loan.company + loan_type_doc.mode_of_payment = loan.mode_of_payment + loan_type_doc.payment_account = loan.payment_account + loan_type_doc.loan_account = loan.loan_account + loan_type_doc.interest_income_account = loan.interest_income_account + loan_type_doc.penalty_income_account = penalty_account + loan_type_doc.submit() + + return loan_type_doc.name diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py new file mode 100644 index 00000000000..7f42cd92e3c --- /dev/null +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -0,0 +1,27 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('stock', 'doctype', 'purchase_receipt') + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') + frappe.reload_doc('stock', 'doctype', 'delivery_note') + frappe.reload_doc('stock', 'doctype', 'delivery_note_item') + + def update_from_return_docs(doctype): + for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}): + # Update original receipt/delivery document from return + return_doc = frappe.get_cached_doc(doctype, return_doc.name) + return_doc.update_prevdoc_status() + return_against = frappe.get_doc(doctype, return_doc.return_against) + return_against.update_billing_status() + + # Set received qty in stock uom in PR, as returned qty is checked against it + frappe.db.sql(""" update `tabPurchase Receipt Item` + set received_stock_qty = received_qty * conversion_factor + where docstatus = 1 """) + + for doctype in ('Purchase Receipt', 'Delivery Note'): + update_from_return_docs(doctype) \ No newline at end of file diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py new file mode 100644 index 00000000000..340bf4947b6 --- /dev/null +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -0,0 +1,136 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.model.utils.rename_field import rename_field + +def execute(): + + frappe.reload_doc('Accounts', 'doctype', 'Salary Component Account') + if frappe.db.has_column('Salary Component Account', 'default_account'): + rename_field("Salary Component Account", "default_account", "account") + + doctype_list = [ + { + 'module':'HR', + 'doctype':'Employee Advance' + }, + { + 'module':'HR', + 'doctype':'Leave Encashment' + }, + { + 'module':'Payroll', + 'doctype':'Additional Salary' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Application' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Claim' + }, + { + 'module':'Payroll', + 'doctype':'Employee Incentive' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Declaration' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Proof Submission' + }, + { + 'module':'Payroll', + 'doctype':'Income Tax Slab' + }, + { + 'module':'Payroll', + 'doctype':'Payroll Entry' + }, + { + 'module':'Payroll', + 'doctype':'Retention Bonus' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure Assignment' + }, + { + 'module':'Payroll', + 'doctype':'Salary Slip' + }, + ] + + for item in doctype_list: + frappe.reload_doc(item['module'], 'doctype', item['doctype']) + + # update company in employee advance based on employee company + for dt in ['Employee Incentive', 'Leave Encashment', 'Employee Benefit Application', 'Employee Benefit Claim']: + frappe.db.sql(""" + update `tab{doctype}` + set company = (select company from tabEmployee where name=`tab{doctype}`.employee) + """.format(doctype=dt)) + + # update exchange rate for employee advance + frappe.db.sql("update `tabEmployee Advance` set exchange_rate=1") + + # get all companies and it's currency + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency", "default_payroll_payable_account"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + default_payroll_payable_account = d.default_payroll_payable_account + + if not default_payroll_payable_account: + default_payroll_payable_account = frappe.db.get_value("Account", + {"account_name": _("Payroll Payable"), "company": company, "account_currency": company_currency, "is_group": 0}) + + # update currency in following doctypes based on company currency + doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', + 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', + 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', + 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] + + for dt in doctypes_for_currency: + frappe.db.sql("""update `tab{doctype}` set currency = %s where company=%s""" + .format(doctype=dt), (company_currency, company)) + + # update fields in payroll entry + frappe.db.sql(""" + update `tabPayroll Entry` + set currency = %s, + exchange_rate = 1, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Structure Assignment + frappe.db.sql(""" + update `tabSalary Structure Assignment` + set currency = %s, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Slip + frappe.db.sql(""" + update `tabSalary Slip` + set currency = %s, + exchange_rate = 1, + base_hour_rate = hour_rate, + base_gross_pay = gross_pay, + base_total_deduction = total_deduction, + base_net_pay = net_pay, + base_rounded_total = rounded_total, + base_total_in_words = total_in_words + where company=%s + """, (company_currency, company)) diff --git a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py index 6e92ffb8a01..910814fd227 100644 --- a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py +++ b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py @@ -7,19 +7,23 @@ import frappe def execute(): parent_list = [] count = 0 - for data in frappe.db.sql(""" - select + + frappe.reload_doc('stock', 'doctype', 'purchase_receipt') + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') + + for data in frappe.db.sql(""" + select `tabPurchase Receipt Item`.purchase_order, `tabPurchase Receipt Item`.name, `tabPurchase Receipt Item`.item_code, `tabPurchase Receipt Item`.idx, `tabPurchase Receipt Item`.parent - from + from `tabPurchase Receipt Item`, `tabPurchase Receipt` where `tabPurchase Receipt Item`.parent = `tabPurchase Receipt`.name and `tabPurchase Receipt Item`.purchase_order_item is null and `tabPurchase Receipt Item`.purchase_order is not null and `tabPurchase Receipt`.is_return = 1""", as_dict=1): - name = frappe.db.get_value('Purchase Order Item', + name = frappe.db.get_value('Purchase Order Item', {'item_code': data.item_code, 'parent': data.purchase_order, 'idx': data.idx}, 'name') if name: diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d56cd4e967d..7737e6c8869 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -13,4 +13,48 @@ frappe.ui.form.on('Additional Salary', { }; }); }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + 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(); + } + } + }); + }, }); diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json index 69cb5da893e..2b29f667fbc 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -11,20 +11,21 @@ "employee", "employee_name", "salary_component", - "overwrite_salary_structure_amount", - "deduct_full_tax_on_selected_payroll_date", + "type", + "amount", "ref_doctype", "ref_docname", + "amended_from", "column_break_5", "company", - "is_recurring", + "department", + "currency", "from_date", "to_date", "payroll_date", - "type", - "department", - "amount", - "amended_from" + "is_recurring", + "overwrite_salary_structure_amount", + "deduct_full_tax_on_selected_payroll_date" ], "fields": [ { @@ -59,6 +60,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 }, { @@ -159,11 +161,22 @@ "label": "Reference Document", "options": "ref_doctype", "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 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 21:10:50.374063", + "modified": "2020-10-20 17:51:13.419716", "modified_by": "Administrator", "module": "Payroll", "name": "Additional Salary", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index e3dc9070ec5..f5af677fce2 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -22,10 +22,15 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_salary_structure() self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + 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 validate_recurring_additional_salary_overlap(self): if self.is_recurring: additional_salaries = frappe.db.sql(""" diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index de26543b571..4d47f25fcf3 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestAdditionalSalary(unittest.TestCase): @@ -15,12 +16,19 @@ class TestAdditionalSalary(unittest.TestCase): def setUp(self): setup_test() + def tearDown(self): + for dt in ["Salary Slip", "Additional Salary", "Salary Structure Assignment", "Salary Structure"]: + frappe.db.sql("delete from `tab%s`" % dt) + def test_recurring_additional_salary(self): + amount = 0 + salary_component = None emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) + salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) add_sal = get_additional_salary(emp_id) - - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly") + + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": amount = earning.amount @@ -29,8 +37,6 @@ class TestAdditionalSalary(unittest.TestCase): self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) - - def get_additional_salary(emp_id): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") @@ -40,6 +46,7 @@ def get_additional_salary(emp_id): add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) add_sal.amount = 5000 + add_sal.currency = erpnext.get_default_currency() add_sal.save() add_sal.submit() diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js index f509df31e83..6756cd93e75 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js @@ -3,7 +3,12 @@ frappe.ui.form.on('Employee Benefit Application', { employee: function(frm) { - frm.trigger('set_earning_component'); + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_earning_component') + ]); + } var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; @@ -38,9 +43,26 @@ frappe.ui.form.on('Employee Benefit Application', { }); }, + get_employee_currency: function(frm) { + if (frm.doc.employee) { + 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(); + } + } + }); + } + }, + payroll_period: function(frm) { var method, args; - if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ + if (frm.doc.employee && frm.doc.date && frm.doc.payroll_period) { method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; args = { employee: frm.doc.employee, @@ -60,11 +82,14 @@ var get_max_benefits=function(frm, method, args) { method: method, args: args, callback: function (data) { - if(!data.exc){ - if(data.message){ + if (!data.exc) { + if (data.message) { frm.set_value("max_benefits", data.message); + } else { + frm.set_value("max_benefits", 0); } } + frm.refresh_fields(); } }); }; @@ -82,14 +107,19 @@ var calculate_all = function(doc) { var tbl = doc.employee_benefits || []; var pro_rata_dispensed_amount = 0; var total_amount = 0; - for(var i = 0; i < tbl.length; i++){ - if(cint(tbl[i].amount) > 0) { - total_amount += flt(tbl[i].amount); - } - if(tbl[i].pay_against_benefit_claim != 1){ - pro_rata_dispensed_amount += flt(tbl[i].amount); + if (doc.max_benefits === 0) { + doc.employee_benefits = []; + } else { + for (var i = 0; i < tbl.length; i++) { + if (cint(tbl[i].amount) > 0) { + total_amount += flt(tbl[i].amount); + } + if (tbl[i].pay_against_benefit_claim != 1) { + pro_rata_dispensed_amount += flt(tbl[i].amount); + } } } + doc.total_amount = total_amount; doc.remaining_benefit = doc.max_benefits - total_amount; doc.pro_rata_dispensed_amount = pro_rata_dispensed_amount; diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index b0c1bd6c3e5..4c45580bf01 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -10,17 +10,20 @@ "field_order": [ "employee", "employee_name", + "currency", "max_benefits", "remaining_benefit", "column_break_2", "date", "payroll_period", "department", + "company", "amended_from", "section_break_4", "employee_benefits", "totals", "total_amount", + "column_break", "pro_rata_dispensed_amount" ], "fields": [ @@ -43,12 +46,14 @@ "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { "fieldname": "remaining_benefit", "fieldtype": "Currency", "label": "Remaining Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { @@ -108,18 +113,42 @@ "fieldname": "total_amount", "fieldtype": "Currency", "label": "Total Amount", + "options": "currency", "read_only": 1 }, { "fieldname": "pro_rata_dispensed_amount", "fieldtype": "Currency", "label": "Dispensed Amount (Pro-rated)", + "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", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "column_break", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:58:31.271922", + "modified": "2020-12-14 15:52:08.566418", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index ef844fbd3b5..27df30a459c 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -33,8 +33,8 @@ class EmployeeBenefitApplication(Document): benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1},\ - set the amount equal or greater than {2}").format(benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( + benefit_claimed, benefit.earning_component, benefit_claim_remining)) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -62,11 +62,11 @@ class EmployeeBenefitApplication(Document): if pro_rata_amount == 0 and non_pro_rata_amount == 0: frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application \ - as pro-rata component").format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( + non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as \ - pro-rata component").format(self.remaining_benefit)) + frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit)) def validate_max_benefit_for_component(self): if self.employee_benefits: @@ -115,7 +115,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): if max_benefits and max_benefits > 0: have_depends_on_payment_days = False per_day_amount_total = 0 - payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[0] + payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[1] payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) # Get all salary slip flexi amount in the payroll period @@ -239,4 +239,17 @@ def get_earning_components(doctype, txt, searchfield, start, page_len, filters): """, salary_structure) else: frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) \ No newline at end of file + .format(filters['employee'], filters['date'])) + +@frappe.whitelist() +def get_earning_components_max_benefits(employee, date, earning_component): + salary_structure = get_assigned_salary_structure(employee, date) + amount = frappe.db.sql(""" + select amount + from `tabSalary Detail` + where parent = %s and is_flexible_benefit = 1 + and salary_component = %s + order by name + """, salary_structure, earning_component) + + return amount if amount else 0 \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json index fa6b4da2af3..c93d356c209 100644 --- a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json +++ b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json @@ -33,6 +33,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Benefit Amount", + "options": "currency", "read_only": 1 }, { @@ -40,12 +41,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:45:00.519134", + "modified": "2020-09-29 16:22:15.783854", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application Detail", diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js index 6db6cb86b3d..ea9ccd52055 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js @@ -12,5 +12,24 @@ frappe.ui.form.on('Employee Benefit Claim', { }, employee: function(frm) { frm.set_value("earning_component", null); + if (frm.doc.employee) { + 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.set_df_property('currency', 'hidden', 0); + } + } + }); + } + if (!frm.doc.earning_component) { + frm.doc.max_amount_eligible = null; + frm.doc.claimed_amount = null; + } + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json index ae4c218615a..da24aacda1b 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -12,6 +12,8 @@ "department", "column_break_3", "claim_date", + "currency", + "company", "benefit_type_and_amount", "earning_component", "max_amount_eligible", @@ -76,6 +78,7 @@ "fieldname": "max_amount_eligible", "fieldtype": "Currency", "label": "Max Amount Eligible", + "options": "currency", "read_only": 1 }, { @@ -92,6 +95,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Claimed Amount", + "options": "currency", "reqd": 1 }, { @@ -119,11 +123,29 @@ "fieldname": "attachments", "fieldtype": "Attach", "label": "Attachments" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 23:01:50.791676", + "modified": "2020-11-25 11:49:56.097352", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Claim", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index db0f83aac9a..182ce0f83a6 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -11,12 +11,57 @@ frappe.ui.form.on('Employee Incentive', { }; }); + if (!frm.doc.company) return; frm.set_query("salary_component", function() { return { - filters: { - "type": "Earning" - } + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", company: frm.doc.company} }; }); - } + + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + 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(); + } + } + }); + }, }); diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json index 204c9a40b1d..e5b1052b3a5 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -7,10 +7,12 @@ "engine": "InnoDB", "field_order": [ "employee", - "incentive_amount", "employee_name", - "salary_component", + "company", + "currency", + "incentive_amount", "column_break_5", + "salary_component", "payroll_date", "department", "amended_from" @@ -28,6 +30,7 @@ "fieldname": "incentive_amount", "fieldtype": "Currency", "label": "Incentive Amount", + "options": "currency", "reqd": 1 }, { @@ -70,11 +73,29 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 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": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:51.209630", + "modified": "2020-10-20 17:22:16.468042", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Incentive", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index 84a97f6bb2e..ead3db126f7 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -4,14 +4,23 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document class EmployeeIncentive(Document): + def validate(self): + self.validate_salary_structure() + + 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 on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = self.employee + additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 additional_salary.amount = self.incentive_amount diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index de7c348bb2c..83d4ae53df8 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -14,6 +14,7 @@ "column_break_2", "payroll_period", "company", + "currency", "amended_from", "section_break_8", "declarations", @@ -92,6 +93,7 @@ "fieldname": "total_declared_amount", "fieldtype": "Currency", "label": "Total Declared Amount", + "options": "currency", "read_only": 1 }, { @@ -102,12 +104,22 @@ "fieldname": "total_exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:49:43.829892", + "modified": "2020-10-20 16:42:24.493761", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9549fd1b757..0609d191497 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -22,6 +22,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -39,6 +40,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -54,6 +56,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -70,6 +73,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json index 8c2f9aa370a..723a3df3c7f 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json @@ -35,6 +35,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exempted Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -43,12 +44,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Declared Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:41:03.638739", + "modified": "2020-10-20 16:43:09.606265", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js index 715d7553b00..497f35c41e3 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js @@ -54,5 +54,9 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', { }); }); } + }, + + currency: function(frm) { + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index b62b5aab0b4..53f18cb1fe3 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -11,6 +11,7 @@ "employee", "employee_name", "department", + "currency", "column_break_2", "submission_date", "payroll_period", @@ -97,6 +98,7 @@ "fieldname": "total_actual_amount", "fieldtype": "Currency", "label": "Total Actual Amount", + "options": "currency", "read_only": 1 }, { @@ -107,6 +109,7 @@ "fieldname": "exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 }, { @@ -126,11 +129,20 @@ "options": "Employee Tax Exemption Proof Submission", "print_hide": 1, "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:53:10.412321", + "modified": "2020-10-20 16:47:03.410020", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json index c1f532050ac..2fd8b94efdb 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json @@ -34,6 +34,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exemption Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -48,12 +49,13 @@ "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Actual Amount" + "label": "Actual Amount", + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:37:08.265600", + "modified": "2020-10-20 16:47:31.480870", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission Detail", diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js index 73a54eb8dd9..7d780d3b040 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js @@ -2,5 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Income Tax Slab', { - + currency: function(frm) { + frm.refresh_fields(); + } }); diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json index 6337d5a6d3e..9fa261dea2d 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json @@ -9,8 +9,9 @@ "effective_from", "company", "column_break_3", - "allow_tax_exemption", + "currency", "standard_tax_exemption_amount", + "allow_tax_exemption", "disabled", "amended_from", "taxable_salary_slabs_section", @@ -70,7 +71,7 @@ "fieldname": "standard_tax_exemption_amount", "fieldtype": "Currency", "label": "Standard Tax Exemption Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "company", @@ -90,11 +91,20 @@ "fieldtype": "Table", "label": "Other Taxes and Charges", "options": "Income Tax Slab Other Charges" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:27:13.425084", + "modified": "2020-10-19 13:54:24.728075", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab", diff --git a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json index 7f21204591a..0dba3382504 100644 --- a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json +++ b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -45,7 +45,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Min Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_7", @@ -57,12 +57,12 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:33:17.931912", + "modified": "2020-10-19 13:45:12.850090", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab Other Charges", diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index bb68e1814a7..8a55224dca7 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -52,7 +52,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 23:25:13.779032", + "modified": "2020-09-30 12:40:07.999878", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 1abc869c539..cb48abbc363 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -17,6 +17,16 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + frm.set_query("payroll_payable_account", function() { + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + } + }; + }); }, refresh: function(frm) { @@ -139,6 +149,36 @@ frappe.ui.form.on('Payroll Entry', { frm.events.clear_employee_table(frm); }, + currency: function (frm) { + 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 (frm.doc.currency) { + if (company_currency != frm.doc.currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: frm.doc.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); + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + } + }, + department: function (frm) { frm.events.clear_employee_table(frm); }, diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 31a899699d7..7a48dd14758 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -11,8 +11,11 @@ "column_break0", "posting_date", "payroll_frequency", - "column_break1", "company", + "column_break1", + "currency", + "exchange_rate", + "payroll_payable_account", "section_break_8", "branch", "department", @@ -257,12 +260,37 @@ { "fieldname": "column_break_33", "fieldtype": "Column Break" + }, + { + "depends_on": "company", + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account", + "reqd": 1 } ], "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:06:06.953904", + "modified": "2020-10-23 13:00:33.753228", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a3d12c35c09..8c2d9740ece 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff @@ -51,13 +51,15 @@ class PayrollEntry(Document): where docstatus = 1 and is_active = 'Yes' - and company = %(company)s and + and company = %(company)s + and currency = %(currency)s and ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s {condition}""".format(condition=condition), - {"company": self.company, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) + {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " + cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " cond += "and %(from_date)s >= t2.from_date" emp_list = frappe.db.sql(""" select @@ -68,14 +70,26 @@ class PayrollEntry(Document): t1.name = t2.employee and t2.docstatus = 1 %s order by t2.from_date desc - """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date}, as_dict=True) + """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) return emp_list def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() if not employees: - frappe.throw(_("No employees for the mentioned criteria")) + error_msg = _("No employees found for the mentioned criteria:
Company: {0}
Currency: {1}
Payroll Payable Account: {2}").format( + frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account)) + if self.branch: + error_msg += "
" + _("Branch: {0}").format(frappe.bold(self.branch)) + if self.department: + error_msg += "
" + _("Department: {0}").format(frappe.bold(self.department)) + if self.designation: + error_msg += "
" + _("Designation: {0}").format(frappe.bold(self.designation)) + if self.start_date: + error_msg += "
" + _("Start date: {0}").format(frappe.bold(self.start_date)) + if self.end_date: + error_msg += "
" + _("End date: {0}").format(frappe.bold(self.end_date)) + frappe.throw(error_msg, title=_("No employees found")) for d in employees: self.append('employees', d) @@ -123,7 +137,9 @@ class PayrollEntry(Document): "posting_date": self.posting_date, "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, - "payroll_entry": self.name + "payroll_entry": self.name, + "exchange_rate": self.exchange_rate, + "currency": self.currency }) if len(emp_list) > 30: frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) @@ -160,10 +176,10 @@ class PayrollEntry(Document): def get_salary_component_account(self, salary_component): account = frappe.db.get_value("Salary Component Account", - {"parent": salary_component, "company": self.company}, "default_account") + {"parent": salary_component, "company": self.company}, "account") if not account: - frappe.throw(_("Please set default account in Salary Component {0}") + frappe.throw(_("Please set account in Salary Component {0}") .format(salary_component)) return account @@ -203,21 +219,11 @@ class PayrollEntry(Document): account_dict[(account, key[1])] = account_dict.get((account, key[1]), 0) + amount return account_dict - def get_default_payroll_payable_account(self): - payroll_payable_account = frappe.get_cached_value('Company', - {"company_name": self.company}, "default_payroll_payable_account") - - if not payroll_payable_account: - frappe.throw(_("Please set Default Payroll Payable Account in Company {0}") - .format(self.company)) - - return payroll_payable_account - def make_accrual_jv_entry(self): self.check_permission('write') earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -230,14 +236,19 @@ class PayrollEntry(Document): journal_entry.posting_date = self.posting_date accounts = [] + currencies = [] payable_amount = 0 + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) # Earnings for acc_cc, amount in earnings.items(): + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) accounts.append({ "account": acc_cc[0], - "debit_in_account_currency": flt(amount, precision), + "debit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), "party_type": '', "cost_center": acc_cc[1] or self.cost_center, "project": self.project @@ -245,25 +256,32 @@ class PayrollEntry(Document): # Deductions for acc_cc, amount in deductions.items(): + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) accounts.append({ "account": acc_cc[0], - "credit_in_account_currency": flt(amount, precision), + "credit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), "cost_center": acc_cc[1] or self.cost_center, "party_type": '', "project": self.project }) # Payable amount + exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) accounts.append({ - "account": default_payroll_payable_account, - "credit_in_account_currency": flt(payable_amount, precision), + "account": payroll_payable_account, + "credit_in_account_currency": flt(payable_amt, precision), + "exchange_rate": flt(exchange_rate), "party_type": '', "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) - journal_entry.title = default_payroll_payable_account + if len(currencies) > 1: + multi_currency = 1 + journal_entry.multi_currency = multi_currency + journal_entry.title = payroll_payable_account journal_entry.save() try: @@ -271,10 +289,24 @@ class PayrollEntry(Document): jv_name = journal_entry.name self.update_salary_slip_status(jv_name = jv_name) except Exception as e: - frappe.msgprint(e) + if type(e) in (str, list, tuple): + frappe.msgprint(e) + raise return jv_name + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): + conversion_rate = 1 + exchange_rate = self.exchange_rate + account_currency = frappe.db.get_value('Account', account, 'account_currency') + if account_currency not in currencies: + currencies.append(account_currency) + if account_currency == company_currency: + conversion_rate = self.exchange_rate + exchange_rate = 1 + amount = flt(amount) * flt(conversion_rate) + return exchange_rate, amount + def make_payment_entry(self): self.check_permission('write') @@ -303,31 +335,43 @@ class PayrollEntry(Document): self.create_journal_entry(salary_slip_total, "salary") def create_journal_entry(self, je_payment_amount, user_remark): - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + accounts = [] + currencies = [] + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) + + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) + accounts.append({ + "account": self.payment_account, + "bank_account": self.bank_account, + "credit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + }) + + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) + accounts.append({ + "account": payroll_payable_account, + "debit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + "reference_type": self.doctype, + "reference_name": self.name + }) + + if len(currencies) > 1: + multi_currency = 1 + journal_entry = frappe.new_doc('Journal Entry') journal_entry.voucher_type = 'Bank Entry' journal_entry.user_remark = _('Payment of {0} from {1} to {2}')\ .format(user_remark, self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + journal_entry.multi_currency = multi_currency - payment_amount = flt(je_payment_amount, precision) - - journal_entry.set("accounts", [ - { - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": payment_amount - }, - { - "account": default_payroll_payable_account, - "debit_in_account_currency": payment_amount, - "reference_type": self.doctype, - "reference_name": self.name - } - ]) + journal_entry.set("accounts", accounts) journal_entry.save(ignore_permissions = True) def update_salary_slip_status(self, jv_name = None): @@ -496,6 +540,21 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): if publish_progress: frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), title = _("Creating Salary Slips...")) + else: + salary_slip_name = frappe.db.sql( + '''SELECT + name + FROM `tabSalary Slip` + WHERE company=%s + AND start_date >= %s + AND end_date <= %s + AND employee = %s + ''', (args.company, args.start_date, args.end_date, emp), as_dict=True) + + salary_slip_doc = frappe.get_doc('Salary Slip', salary_slip_name[0].name) + salary_slip_doc.exchange_rate = args.exchange_rate + salary_slip_doc.set_totals() + salary_slip_doc.db_update() payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) payroll_entry.db_set("salary_slips_created", 1) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index b0f225d909d..54106c8d166 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -10,8 +10,8 @@ from frappe.utils import add_months from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ - make_earning_salary_component, make_deduction_salary_component, create_account -from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans @@ -34,10 +34,47 @@ class TestPayrollEntry(unittest.TestCase): get_salary_component_account(data.name) employee = frappe.db.get_value("Employee", {'company': company}) - make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company) + company_doc = frappe.get_doc('Company', company) + make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company, currency=company_doc.default_currency) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date) + make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency) + + def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use + company = erpnext.get_default_company() + employee = make_employee("test_muti_currency_employee@payroll.com", company=company) + for data in frappe.get_all('Salary Component', fields = ["name"]): + if not frappe.db.get_value('Salary Component Account', + {'parent': data.name, 'company': company}, 'name'): + get_salary_component_account(data.name) + + company_doc = frappe.get_doc('Company', company) + salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD') + create_salary_structure_assignment(employee, salary_structure.name, company=company) + frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"}))) + salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure") + dates = get_start_end_dates('Monthly', nowdate()) + payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70) + payroll_entry.make_payment_entry() + + salary_slip.load_from_db() + + payroll_je = salary_slip.journal_entry + payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) + + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) + + payment_entry = frappe.db.sql(''' + Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea + Where je.name = jea.parent + And jea.reference_name = %s + ''', (payroll_entry.name), as_dict=1) + + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use for data in frappe.get_all('Salary Component', fields = ["name"]): @@ -52,24 +89,32 @@ class TestPayrollEntry(unittest.TestCase): "company": "_Test Company" }).insert() + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) + employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", department="cc - _TC", company="_Test Company") employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", department="cc - _TC", company="_Test Company") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company") - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company") - if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC") - frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", - "_Test Payroll Payable - _TC") + create_account(account_name="_Test Payroll Payable", + company="_Test Company", parent_account="Current Liabilities - _TC") + + if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ + frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": + frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", + "_Test Payroll Payable - _TC") + + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, - department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") je_entries = frappe.db.sql(""" select account, cost_center, debit, credit @@ -121,7 +166,7 @@ class TestPayrollEntry(unittest.TestCase): employee_doc.save() salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company") + make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -133,8 +178,8 @@ class TestPayrollEntry(unittest.TestCase): dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(company="_Test Company", start_date=dates.start_date, - end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") + make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") name = frappe.db.get_value('Salary Slip', {'posting_date': nowdate(), 'employee': applicant}, 'name') @@ -165,6 +210,9 @@ def make_payroll_entry(**args): payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None payroll_entry.department = args.department or None + payroll_entry.payroll_payable_account = args.payable_account + payroll_entry.currency = args.currency + payroll_entry.exchange_rate = args.exchange_rate or 1 if args.cost_center: payroll_entry.cost_center = args.cost_center @@ -212,3 +260,11 @@ def make_holiday(holiday_list_name): }).insert() return holiday_list_name + +def get_salary_slip(user, period, salary_structure): + salary_slip = make_employee_salary_slip(user, period, salary_structure) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + salary_slip.db_update() + + return salary_slip \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js index 8ff55151f6f..092cbd89748 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js +++ b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js @@ -9,45 +9,45 @@ QUnit.test("test: Set Salary Components", function (assert) { () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Basic'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Income Tax'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Arrear'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Company', 'For Testing'), () => cur_frm.set_value('default_payroll_payable_account', 'Payroll Payable - FT'), diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js index 64e726db857..f8bb40a9cb8 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js @@ -4,9 +4,13 @@ frappe.ui.form.on('Retention Bonus', { setup: function(frm) { frm.set_query("employee", function() { + if (!frm.doc.company) { + frappe.msgprint(__("Please Select Company First")); + } return { filters: { - "status": "Active" + "status": "Active", + "company": frm.doc.company } }; }); @@ -18,5 +22,22 @@ frappe.ui.form.on('Retention Bonus', { } }; }); + }, + + employee: function(frm) { + if (frm.doc.employee) { + 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(); + } + } + }); + } } }); diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json index da884c2f289..66472300788 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -17,7 +17,8 @@ "column_break_6", "employee_name", "department", - "date_of_joining" + "date_of_joining", + "currency" ], "fields": [ { @@ -46,6 +47,7 @@ "fieldname": "bonus_amount", "fieldtype": "Currency", "label": "Bonus Amount", + "options": "currency", "reqd": 1 }, { @@ -89,11 +91,22 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 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 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:05.251951", + "modified": "2020-10-20 17:27:47.003134", "modified_by": "Administrator", "module": "Payroll", "name": "Retention Bonus", @@ -151,7 +164,6 @@ "share": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index c455eb3303b..dbf75140ac1 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { - frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { + frm.set_query("account", "accounts", function(doc, cdt, cdn) { var d = locals[cdt][cdn]; return { filters: { diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index eedb56ec08f..5c1eb61281c 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -147,7 +147,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "default": "0", @@ -160,7 +160,7 @@ "fieldname": "default_amount", "fieldtype": "Currency", "label": "Default Amount", - "options": "Company:company:default_currency", + "options": "currency", "print_hide": 1 }, { @@ -169,6 +169,7 @@ "hidden": 1, "label": "Additional Amount", "no_copy": 1, + "options": "currency", "print_hide": 1, "read_only": 1 }, @@ -177,6 +178,7 @@ "fieldname": "tax_on_flexible_benefit", "fieldtype": "Currency", "label": "Tax on flexible benefit", + "options": "currency", "read_only": 1 }, { @@ -184,6 +186,7 @@ "fieldname": "tax_on_additional_salary", "fieldtype": "Currency", "label": "Tax on additional salary", + "options": "currency", "read_only": 1 }, { @@ -227,7 +230,7 @@ ], "istable": 1, "links": [], - "modified": "2020-10-07 20:39:41.619283", + "modified": "2020-11-25 13:12:41.081106", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 7b69dbe8d6d..f7e22c63879 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -13,12 +13,12 @@ frappe.ui.form.on("Salary Slip", { ]; }); - frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){ + frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() { return { filters: { employee: frm.doc.employee } - } + }; }; frm.set_query("salary_component", "earnings", function() { @@ -26,7 +26,7 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "earning" } - } + }; }); frm.set_query("salary_component", "deductions", function() { @@ -34,18 +34,18 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "deduction" } - } + }; }); frm.set_query("employee", function() { - return{ + return { query: "erpnext.controllers.queries.employee_query" - } + }; }); }, - start_date: function(frm){ - if(frm.doc.start_date){ + start_date: function(frm) { + if (frm.doc.start_date) { frm.trigger("set_end_date"); } }, @@ -54,7 +54,7 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - set_end_date: function(frm){ + set_end_date: function(frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -66,22 +66,93 @@ frappe.ui.form.on("Salary Slip", { frm.set_value('end_date', r.message.end_date); } } - }) + }); }, company: function(frm) { var company = locals[':Company'][frm.doc.company]; - if(!frm.doc.letter_head && company.default_letter_head) { + if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } + frm.trigger("set_dynamic_labels"); + }, + + set_dynamic_labels: function(frm) { + var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency"); + frappe.run_serially([ + () => frm.events.set_exchange_rate(frm, company_currency), + () => frm.events.change_form_labels(frm, company_currency), + () => frm.events.change_grid_labels(frm), + () => frm.refresh_fields() + ]); + }, + + set_exchange_rate: function(frm, company_currency) { + if (frm.doc.docstatus === 0) { + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + if (from_currency != company_currency) { + frm.events.hide_loan_section(frm); + 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); + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + } + } + }, + + exchange_rate: function(frm) { + calculate_totals(frm); + }, + + hide_loan_section: function(frm) { + frm.set_df_property('section_break_43', 'hidden', 1); + }, + + change_form_labels: function(frm, company_currency) { + frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + company_currency); + + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.doc.currency); + + // toggle fields + frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + frm.doc.currency != company_currency); + }, + + change_grid_labels: function(frm) { + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "deductions"); }, refresh: function(frm) { - frm.trigger("toggle_fields") + frm.trigger("toggle_fields"); var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; - cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false); - cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false); + frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); + frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); + calculate_totals(frm); + frm.trigger("set_dynamic_labels"); }, salary_slip_based_on_timesheet: function(frm) { @@ -98,12 +169,12 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - leave_without_pay: function(frm){ + leave_without_pay: function(frm) { if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ method: 'process_salary_based_on_working_days', doc: frm.doc, - callback: function(r, rt) { + callback: function() { frm.refresh(); } }); @@ -118,51 +189,94 @@ frappe.ui.form.on("Salary Slip", { }, get_emp_and_working_day_details: function(frm) { - return frappe.call({ - method: 'get_emp_and_working_day_details', - doc: frm.doc, - callback: function(r, rt) { - frm.refresh(); - if (r.message){ - frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); + if (frm.doc.employee) { + return frappe.call({ + method: 'get_emp_and_working_day_details', + doc: frm.doc, + callback: function(r) { + if (r.message[1] !== "Leave" && r.message[0]) { + frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as {0}. You can can change this in {1}", [r.message, frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)])); + } + frm.refresh(); } - } - }); + }); + } } }); frappe.ui.form.on('Salary Slip Timesheet', { - time_sheet: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + time_sheet: function(frm) { + calculate_totals(frm); }, - timesheets_remove: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + timesheets_remove: function(frm) { + calculate_totals(frm); } }); -// calculate total working hours, earnings based on hourly wages and totals -var total_work_hours = function(frm, dt, dn) { - var total_working_hours = 0.0; - $.each(frm.doc["timesheets"] || [], function(i, timesheet) { - total_working_hours += timesheet.working_hours; - }); - frm.set_value('total_working_hours', total_working_hours); - - var wages_amount = frm.doc.total_working_hours * frm.doc.hour_rate; - - frappe.db.get_value('Salary Structure', {'name': frm.doc.salary_structure}, 'salary_component', (r) => { - var gross_pay = 0.0; - $.each(frm.doc["earnings"], function(i, earning) { - if (earning.salary_component == r.salary_component) { - earning.amount = wages_amount; - frm.refresh_fields('earnings'); +var calculate_totals = function(frm) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); } - gross_pay += earning.amount; }); - frm.set_value('gross_pay', gross_pay); + } +}; - frm.doc.net_pay = flt(frm.doc.gross_pay) - flt(frm.doc.total_deduction); - frm.doc.rounded_total = Math.round(frm.doc.net_pay); - refresh_many(['net_pay', 'rounded_total']); - }); -} +frappe.ui.form.on('Salary Detail', { + amount: function(frm) { + calculate_totals(frm); + }, + + earnings_remove: function(frm) { + calculate_totals(frm); + }, + + deductions_remove: function(frm) { + calculate_totals(frm); + }, + + salary_component: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.salary_component) { + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Salary Component", + name: child.salary_component + }, + callback: function(data) { + if (data.message) { + var result = data.message; + frappe.model.set_value(cdt, cdn, 'condition', result.condition); + frappe.model.set_value(cdt, cdn, 'amount_based_on_formula', result.amount_based_on_formula); + if (result.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'formula', result.formula); + } else { + frappe.model.set_value(cdt, cdn, 'amount', result.amount); + } + frappe.model.set_value(cdt, cdn, 'statistical_component', result.statistical_component); + frappe.model.set_value(cdt, cdn, 'depends_on_payment_days', result.depends_on_payment_days); + frappe.model.set_value(cdt, cdn, 'do_not_include_in_total', result.do_not_include_in_total); + frappe.model.set_value(cdt, cdn, 'variable_based_on_taxable_salary', result.variable_based_on_taxable_salary); + frappe.model.set_value(cdt, cdn, 'is_tax_applicable', result.is_tax_applicable); + frappe.model.set_value(cdt, cdn, 'is_flexible_benefit', result.is_flexible_benefit); + refresh_field("earnings"); + refresh_field("deductions"); + } + } + }); + } + }, + + amount_based_on_formula: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'amount', null); + } else { + frappe.model.set_value(cdt, cdn, 'formula', null); + } + } +}); diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 619c45fa4a1..386618cf083 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -18,6 +18,8 @@ "journal_entry", "payroll_entry", "company", + "currency", + "exchange_rate", "letter_head", "section_break_10", "start_date", @@ -38,6 +40,7 @@ "column_break_20", "total_working_hours", "hour_rate", + "base_hour_rate", "section_break_26", "bank_name", "bank_account_no", @@ -52,8 +55,10 @@ "deductions", "totals", "gross_pay", + "base_gross_pay", "column_break_25", "total_deduction", + "base_total_deduction", "loan_repayment", "loans", "section_break_43", @@ -63,10 +68,15 @@ "total_loan_repayment", "net_pay_info", "net_pay", + "base_net_pay", "column_break_53", "rounded_total", + "base_rounded_total", "section_break_55", "total_in_words", + "column_break_69", + "base_total_in_words", + "section_break_75", "amended_from" ], "fields": [ @@ -205,9 +215,13 @@ { "fieldname": "salary_structure", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Salary Structure", "options": "Salary Structure", - "read_only": 1 + "read_only": 1, + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:(!doc.salary_slip_based_on_timesheet)", @@ -265,7 +279,7 @@ "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", + "options": "currency", "print_hide_if_no_value": 1 }, { @@ -347,24 +361,13 @@ "fieldname": "gross_pay", "fieldtype": "Currency", "label": "Gross Pay", - "oldfieldname": "gross_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "column_break_25", "fieldtype": "Column Break" }, - { - "fieldname": "total_deduction", - "fieldtype": "Currency", - "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "depends_on": "total_loan_repayment", "fieldname": "loan_repayment", @@ -379,6 +382,7 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.docstatus != 0", "fieldname": "section_break_43", "fieldtype": "Section Break" }, @@ -416,13 +420,10 @@ "label": "net pay info" }, { - "description": "Gross Pay - Total Deduction - Loan Repayment", "fieldname": "net_pay", "fieldtype": "Currency", "label": "Net Pay", - "oldfieldname": "net_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -434,22 +435,13 @@ "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "section_break_55", "fieldtype": "Section Break" }, - { - "description": "Net Pay (in words) will be visible once you save the Salary Slip.", - "fieldname": "total_in_words", - "fieldtype": "Data", - "label": "Total in words", - "oldfieldname": "net_pay_in_words", - "oldfieldtype": "Data", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -500,13 +492,99 @@ { "fieldname": "column_break_18", "fieldtype": "Column Break" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "total_in_words", + "fieldtype": "Data", + "label": "Total in words", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "section_break_75", + "fieldtype": "Section Break" + }, + { + "fieldname": "base_hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide_if_no_value": 1 + }, + { + "fieldname": "base_gross_pay", + "fieldtype": "Currency", + "label": "Gross Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "1.0", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "base_total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_net_pay", + "fieldtype": "Currency", + "label": "Net Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "base_rounded_total", + "fieldtype": "Currency", + "label": "Rounded Total (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_total_in_words", + "fieldtype": "Data", + "label": "Total in words (Company Currency)", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "column_break_69", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-08-11 17:37:54.274384", + "modified": "2020-10-21 23:02:59.400249", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index cecb8cde7c2..20365b191d0 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -50,16 +50,20 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() - company_currency = erpnext.get_company_currency(self.company) - total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total - self.total_in_words = money_in_words(total, company_currency) - if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). format(max_working_hours), alert=True) + def set_net_total_in_words(self): + doc_currency = self.currency + company_currency = erpnext.get_company_currency(self.company) + total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total + base_total = self.base_net_pay if self.is_rounding_total_disabled() else self.base_rounded_total + self.total_in_words = money_in_words(total, doc_currency) + self.base_total_in_words = money_in_words(base_total, company_currency) + def on_submit(self): if self.net_pay < 0: frappe.throw(_("Net Pay cannot be less than 0")) @@ -136,8 +140,8 @@ class SalarySlip(TransactionBase): self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.set_time_sheet() self.pull_sal_struct() - consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" - return consider_unmarked_attendance_as + payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) + return [payroll_based_on, consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -182,6 +186,7 @@ class SalarySlip(TransactionBase): if self.salary_slip_based_on_timesheet: self.salary_structure = self._salary_structure_doc.name self.hour_rate = self._salary_structure_doc.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 wages_amount = self.hour_rate * self.total_working_hours @@ -210,10 +215,10 @@ class SalarySlip(TransactionBase): frappe.throw(_("Please set Payroll based on in Payroll settings")) if payroll_based_on == "Attendance": - actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays) + actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays) self.absent_days = absent else: - actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days) if not lwp: lwp = actual_lwp @@ -300,7 +305,7 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp_based_on_leave_application(self, holidays, working_days): + def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) daily_wages_fraction_for_half_day = \ @@ -311,10 +316,12 @@ class SalarySlip(TransactionBase): leave = frappe.db.sql(""" SELECT t1.name, CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) - THEN t1.half_day else 0 END + THEN t1.half_day else 0 END, + t2.is_ppl, + t2.fraction_of_daily_salary_per_leave FROM `tabLeave Application` t1, `tabLeave Type` t2 WHERE t2.name = t1.leave_type - AND t2.is_lwp = 1 + AND (t2.is_lwp = 1 or t2.is_ppl = 1) AND t1.docstatus = 1 AND t1.employee = %(employee)s AND ifnull(t1.salary_slip, '') = '' @@ -327,19 +334,35 @@ class SalarySlip(TransactionBase): """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: + equivalent_lwp_count = 0 is_half_day_leave = cint(leave[0][1]) - lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + is_partially_paid_leave = cint(leave[0][2]) + fraction_of_daily_salary_per_leave = flt(leave[0][3]) + + equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + if is_partially_paid_leave: + equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + + lwp += equivalent_lwp_count return lwp - def calculate_lwp_and_absent_days_based_on_attendance(self, holidays): + def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays): lwp = 0 absent = 0 daily_wages_fraction_for_half_day = \ flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 - lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + leave_types = frappe.get_all("Leave Type", + or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]], + fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"]) + + leave_type_map = {} + for leave_type in leave_types: + leave_type_map[leave_type.name] = leave_type + attendances = frappe.db.sql(''' SELECT attendance_date, status, leave_type FROM `tabAttendance` @@ -351,21 +374,30 @@ class SalarySlip(TransactionBase): ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) for d in attendances: - if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys(): continue if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: if d.status == "Absent" or \ - (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']): continue + if d.leave_type: + fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"] + if d.status == "Half Day": - lwp += (1 - daily_wages_fraction_for_half_day) - elif d.status == "On Leave" and d.leave_type in lwp_leave_types: - lwp += 1 + equivalent_lwp = (1 - daily_wages_fraction_for_half_day) + + if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp + elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys(): + equivalent_lwp = 1 + if leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp elif d.status == "Absent": absent += 1 - return lwp, absent def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -390,15 +422,22 @@ class SalarySlip(TransactionBase): if self.salary_structure: self.calculate_component_amounts("earnings") self.gross_pay = self.get_component_totals("earnings") + self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) if self.salary_structure: self.calculate_component_amounts("deductions") self.total_deduction = self.get_component_totals("deductions") + self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) self.set_loan_repayment() self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) + self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay')) + if self.hour_rate: + self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate')) + self.set_net_total_in_words() def calculate_component_amounts(self, component_type): if not getattr(self, '_salary_structure_doc', None): @@ -949,9 +988,9 @@ class SalarySlip(TransactionBase): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] if payment.total_payment > total_amount: - frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} - against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment), - frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""") + .format(payment.idx, frappe.bold(payment.total_payment), + frappe.bold(total_amount), frappe.bold(payment.loan))) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount @@ -1046,6 +1085,46 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() + def set_totals(self): + self.gross_pay = 0 + if self.salary_slip_based_on_timesheet == 1: + self.calculate_total_for_salary_slip_based_on_timesheet() + else: + self.total_deduction = 0 + if self.earnings: + for earning in self.earnings: + self.gross_pay += flt(earning.amount) + if self.deductions: + for deduction in self.deductions: + self.total_deduction += flt(deduction.amount) + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) + self.set_base_totals() + + def set_base_totals(self): + self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) + self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate) + self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate) + self.base_rounded_total = rounded(self.base_net_pay) + self.set_net_total_in_words() + + #calculate total working hours, earnings based on hourly wages and totals + def calculate_total_for_salary_slip_based_on_timesheet(self): + if self.timesheets: + for timesheet in self.timesheets: + if timesheet.working_hours: + self.total_working_hours += timesheet.working_hours + + wages_amount = self.total_working_hours * self.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) + salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component') + if self.earnings: + for i, earning in enumerate(self.earnings): + if earning.salary_component == salary_component: + self.earnings[i].amount = wages_amount + self.gross_pay += self.earnings[i].amount + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 7fe4165362c..5daf1d439d1 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -13,6 +13,8 @@ from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \ import create_payroll_period, create_exemption_category @@ -31,7 +33,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75) - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -53,7 +55,7 @@ class TestSalarySlip(unittest.TestCase): mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp - ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days_based_on_attendance@salary.com", "Monthly", "Test Payment Based On Attendence") self.assertEqual(ss.leave_without_pay, 1.25) self.assertEqual(ss.absent_days, 1) @@ -76,7 +78,7 @@ class TestSalarySlip(unittest.TestCase): # Payroll based on attendance frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -93,31 +95,40 @@ class TestSalarySlip(unittest.TestCase): make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") - ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1) + leave_type_ppl.save() - self.assertEqual(ss.leave_without_pay, 3) + alloc = create_leave_allocation( + employee = emp_id, from_date = add_days(first_sunday, 4), + to_date = add_days(first_sunday, 10), new_leaves_allocated = 3, + leave_type = "Test Partially Paid Leave") + alloc.save() + alloc.submit() + + #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp + make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") + + ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application") + + + self.assertEqual(ss.leave_without_pay, 4) days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] - self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) - - #Gross pay calculation based on attendances - gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) - - self.assertEqual(flt(ss.gross_pay, 2), flt(gross_pay, 2)) + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_included@salary.com", "Monthly", "Test Salary Slip With Holidays Included") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, no_of_days[0]) @@ -128,12 +139,12 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_excluded@salary.com", "Monthly", "Test Salary Slip With Holidays Excluded") self.assertEqual(ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(ss.payment_days, no_of_days[0] - no_of_days[1]) @@ -148,7 +159,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month - make_employee("test_employee@salary.com") + make_employee("test_payment_days@salary.com") if getdate(nowdate()).day >= 15: relieving_date = getdate(add_days(nowdate(),-10)) date_of_joining = getdate(add_days(nowdate(),-10)) @@ -163,39 +174,39 @@ class TestSalarySlip(unittest.TestCase): relieving_date = getdate(nowdate()) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", date_of_joining) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", date_of_joining) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", "Test Payment Days") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) # set relieving date in the same month frappe.db.set_value("Employee",frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", relieving_date) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", relieving_date) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Left") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Left") ss.save() self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, getdate(relieving_date).day) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") def test_employee_salary_slip_read_permission(self): - make_employee("test_employee@salary.com") + make_employee("test_employee_salary_slip_read_permission@salary.com") - salary_slip_test_employee = make_employee_salary_slip("test_employee@salary.com", "Monthly") - frappe.set_user("test_employee@salary.com") + salary_slip_test_employee = make_employee_salary_slip("test_employee_salary_slip_read_permission@salary.com", "Monthly", "Test Employee Salary Slip Read Permission") + frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) def test_email_salary_slip(self): @@ -203,8 +214,8 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) - make_employee("test_employee@salary.com") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + make_employee("test_email_salary_slip@salary.com") + ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() ss.submit() @@ -215,8 +226,9 @@ class TestSalarySlip(unittest.TestCase): def test_loan_repayment_salary_slip(self): from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - applicant = make_employee("test_loanemployee@salary.com", company="_Test Company") + applicant = make_employee("test_loan_repayment_salary_slip@salary.com", company="_Test Company") create_loan_accounts() @@ -228,6 +240,8 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() @@ -236,7 +250,7 @@ class TestSalarySlip(unittest.TestCase): process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure") ss.submit() self.assertEqual(ss.total_loan_repayment, 592) @@ -249,7 +263,7 @@ class TestSalarySlip(unittest.TestCase): for payroll_frequency in ["Monthly", "Bimonthly", "Fortnightly", "Weekly", "Daily"]: make_employee(payroll_frequency + "_test_employee@salary.com") - ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency) + ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency, payroll_frequency + "_Test Payroll Frequency") if payroll_frequency == "Monthly": self.assertEqual(ss.end_date, m['month_end_date']) elif payroll_frequency == "Bimonthly": @@ -264,6 +278,18 @@ class TestSalarySlip(unittest.TestCase): elif payroll_frequency == "Daily": self.assertEqual(ss.end_date, nowdate()) + def test_multi_currency_salary_slip(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") + frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") + salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') + salary_slip = make_salary_slip(salary_structure.name, employee = applicant) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + + self.assertEqual(salary_slip.gross_pay, 78000) + self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -384,16 +410,21 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) + if not frappe.db.exists('Salary Structure', salary_structure): + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) + else: + salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) - if not salary_slip: + if not salary_slip_name: salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) salary_slip.employee_name = frappe.get_value("Employee", {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() + else: + salary_slip = frappe.get_doc('Salary Slip', salary_slip_name) return salary_slip @@ -434,7 +465,7 @@ def get_salary_component_account(sal_comp, company_list=None): sal_comp.append("accounts", { "company": d, - "default_account": create_account(account_name, d, parent_account) + "account": create_account(account_name, d, parent_account) }) sal_comp.save() @@ -561,7 +592,8 @@ def create_exemption_declaration(employee, payroll_period): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "payroll_period": payroll_period, - "company": erpnext.get_default_company() + "company": erpnext.get_default_company(), + "currency": erpnext.get_default_currency() }) declaration.append("declarations", { "exemption_sub_category": "_Test Sub Category", @@ -576,7 +608,8 @@ def create_proof_submission(employee, payroll_period, amount): "doctype": "Employee Tax Exemption Proof Submission", "employee": employee, "payroll_period": payroll_period.name, - "submission_date": submission_date + "submission_date": submission_date, + "currency": erpnext.get_default_currency() }) proof_submission.append("tax_exemption_proofs", { "exemption_sub_category": "_Test Sub Category", @@ -593,13 +626,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): "employee": employee, "claimed_amount": amount, "claim_date": claim_date, - "earning_component": component + "earning_component": component, + "currency": erpnext.get_default_currency() }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): - if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): - return +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): + frappe.db.sql("""delete from `tabIncome Tax Slab`""") slabs = [ { @@ -622,6 +655,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.currency = currency if allow_tax_exemption: income_tax_slab.allow_tax_exemption = 1 @@ -672,7 +706,8 @@ def create_additional_salary(employee, payroll_period, amount): "salary_component": "Performance Bonus", "payroll_date": salary_date, "amount": amount, - "type": "Earning" + "type": "Earning", + "currency": erpnext.get_default_currency() }).submit() return salary_date diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ad93a2fa4bf..ba824c5d6fa 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -41,20 +41,6 @@ frappe.ui.form.on('Salary Structure', { frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet) - frm.set_query("salary_component", "earnings", function() { - return { - filters: { - type: "earning" - } - } - }); - frm.set_query("salary_component", "deductions", function() { - return { - filters: { - type: "deduction" - } - } - }); frm.set_query("payment_account", function () { var account_types = ["Bank", "Cash"]; return { @@ -65,9 +51,47 @@ frappe.ui.form.on('Salary Structure', { } }; }); + frm.trigger('set_earning_deduction_component'); + }, + + set_earning_deduction_component: function(frm) { + if(!frm.doc.company) return; + frm.set_query("salary_component", "earnings", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", company: frm.doc.company} + }; + }); + frm.set_query("salary_component", "deductions", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "deduction", company: frm.doc.company} + }; + }); + }, + + + currency: function(frm) { + calculate_totals(frm.doc); + frm.trigger("set_dynamic_labels") + frm.refresh() + }, + + set_dynamic_labels: function(frm) { + frm.set_currency_labels(["net_pay","hour_rate", "leave_encashment_amount_per_day", "max_benefits", "total_earning", + "total_deduction"], frm.doc.currency); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "deductions"); + + frm.refresh_fields(); }, refresh: function(frm) { + frm.trigger("set_dynamic_labels") frm.trigger("toggle_fields"); frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false); frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false); @@ -101,10 +125,12 @@ frappe.ui.form.on('Salary Structure', { fields: [ {fieldname: "sec_break", fieldtype: "Section Break", label: __("Filter Employees By (Optional)")}, {fieldname: "company", fieldtype: "Link", options: "Company", label: __("Company"), default: frm.doc.company, read_only:1}, + {fieldname: "currency", fieldtype: "Link", options: "Currency", label: __("Currency"), default: frm.doc.currency, read_only:1}, {fieldname: "grade", fieldtype: "Link", options: "Employee Grade", label: __("Employee Grade")}, {fieldname:'department', fieldtype:'Link', options: 'Department', label: __('Department')}, {fieldname:'designation', fieldtype:'Link', options: 'Designation', label: __('Designation')}, - {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"payroll_payable_account", fieldtype: "Link", options: "Account", filters: {"company": frm.doc.company, "root_type": "Liability", "is_group": 0, "account_currency": frm.doc.currency}, label: __("Payroll Payable Account")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index 5f94929f0b5..de56fc8457e 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -13,6 +13,7 @@ "column_break1", "is_active", "payroll_frequency", + "currency", "is_default", "time_sheet_earning_detail", "salary_slip_based_on_timesheet", @@ -26,9 +27,9 @@ "deductions", "conditions_and_formula_variable_and_example", "net_pay_detail", - "column_break2", "total_earning", "total_deduction", + "column_break2", "net_pay", "account", "mode_of_payment", @@ -43,23 +44,17 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head", - "show_days": 1, - "show_seconds": 1 + "options": "Letter Head" }, { "fieldname": "column_break1", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -72,9 +67,7 @@ "oldfieldname": "is_active", "oldfieldtype": "Select", "options": "\nYes\nNo", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "default": "Monthly", @@ -82,9 +75,7 @@ "fieldname": "payroll_frequency", "fieldtype": "Select", "label": "Payroll Frequency", - "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily", - "show_days": 1, - "show_seconds": 1 + "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily" }, { "default": "No", @@ -95,62 +86,46 @@ "no_copy": 1, "options": "Yes\nNo", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "time_sheet_earning_detail", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "0", "fieldname": "salary_slip_based_on_timesheet", "fieldtype": "Check", - "label": "Salary Slip Based on Timesheet", - "show_days": 1, - "show_seconds": 1 + "label": "Salary Slip Based on Timesheet" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "description": "Salary Component for timesheet based payroll.", "fieldname": "salary_component", "fieldtype": "Link", "label": "Salary Component", - "options": "Salary Component", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Component" }, { "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "leave_encashment_amount_per_day", "fieldtype": "Currency", "label": "Leave Encashment Amount Per Day", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Amount)", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "description": "Salary breakup based on Earning and Deduction.", @@ -158,9 +133,7 @@ "fieldtype": "Section Break", "oldfieldname": "earning_deduction", "oldfieldtype": "Section Break", - "precision": "2", - "show_days": 1, - "show_seconds": 1 + "precision": "2" }, { "fieldname": "earnings", @@ -168,9 +141,7 @@ "label": "Earnings", "oldfieldname": "earning_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "deductions", @@ -178,22 +149,16 @@ "label": "Deductions", "oldfieldname": "deduction_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "net_pay_detail", "fieldtype": "Section Break", - "options": "Simple", - "show_days": 1, - "show_seconds": 1 + "options": "Simple" }, { "fieldname": "column_break2", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -201,63 +166,45 @@ "fieldtype": "Currency", "hidden": 1, "label": "Total Earning", - "oldfieldname": "total_earning", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "total_deduction", "fieldtype": "Currency", "hidden": 1, "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "net_pay", "fieldtype": "Currency", "hidden": 1, "label": "Net Pay", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "account", "fieldtype": "Section Break", - "label": "Account", - "show_days": 1, - "show_seconds": 1 + "label": "Account" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", - "options": "Mode of Payment", - "show_days": 1, - "show_seconds": 1 + "options": "Mode of Payment" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "amended_from", @@ -266,23 +213,26 @@ "no_copy": 1, "options": "Salary Structure", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "conditions_and_formula_variable_and_example", "fieldtype": "HTML", - "label": "Conditions and Formula variable and example", - "show_days": 1, - "show_seconds": 1 + "label": "Conditions and Formula variable and example" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "reqd": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-22 17:07:26.129355", + "modified": "2020-09-30 11:30:32.190798", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ffc16d73c25..77914bb5319 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt, cint, cstr from frappe import _ @@ -88,24 +88,26 @@ class SalaryStructure(Document): return employees @frappe.whitelist() - def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None, variable=None, income_tax_slab=None): - employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) + def assign_salary_structure(self, grade=None, department=None, designation=None,employee=None, + payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): + employees = self.get_employees(company= self.company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + employees=employees, salary_structure=self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + assign_salary_structure_for_employees(employees, self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): +def assign_salary_structure_for_employees(employees, salary_structure, payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -115,7 +117,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date count +=1 salary_structures_assignment = create_salary_structures_assignment(employee, - salary_structure, from_date, base, variable, income_tax_slab) + salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -123,11 +125,22 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): +def create_salary_structures_assignment(employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab=None): + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', salary_structure.company, 'default_payroll_payable_account') + if not payroll_payable_account: + frappe.throw(_('Please set "Default Payroll Payable Account" in Company Defaults')) + payroll_payable_account_currency = frappe.db.get_value('Account', payroll_payable_account, 'account_currency') + company_curency = erpnext.get_company_currency(salary_structure.company) + if payroll_payable_account_currency != salary_structure.currency and payroll_payable_account_currency != company_curency: + frappe.throw(_("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format(salary_structure.currency, company_curency)) + assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name assignment.company = salary_structure.company + assignment.currency = salary_structure.currency + assignment.payroll_payable_account = payroll_payable_account assignment.from_date = from_date assignment.base = base assignment.variable = variable @@ -170,7 +183,8 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "doctype": "Salary Slip", "field_map": { "total_earning": "gross_pay", - "name": "salary_structure" + "name": "salary_structure", + "currency": "currency" } } }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) @@ -188,7 +202,22 @@ def get_employees(salary_structure): filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee']) if not employees: - frappe.throw(_("There's no Employee with Salary Structure: {0}. \ - Assign {1} to an Employee to preview Salary Slip").format(salary_structure, salary_structure)) + frappe.throw(_("There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip").format( + salary_structure, salary_structure)) return list(set([d.employee for d in employees])) + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): + if len(filters) < 2: + return {} + + return frappe.db.sql(""" + select t1.salary_component + from `tabSalary Component` t1, `tabSalary Component Account` t2 + where t1.salary_component = t2.parent + and t1.type = %s + and t2.company = %s + order by salary_component + """, (filters['type'], filters['company']) ) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e04fda81202..abb669740b6 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -94,7 +94,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertFalse(("\n" in row.formula) or ("\n" in row.condition)) def test_salary_structures_assignment(self): - salary_structure = make_salary_structure("Salary Structure Sample", "Monthly") + company_currency = erpnext.get_default_currency() + salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", currency=company_currency) employee = "test_assign_stucture@salary.com" employee_doc_name = make_employee(employee) # clear the already assigned stuctures @@ -107,8 +108,13 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) + def test_multi_currency_salary_structure(self): + make_employee("test_muti_currency_employee@salary.com") + sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') + self.assertEqual(sal_struct.currency, 'USD') + def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None): + test_tax=False, company=None, currency=erpnext.get_default_currency()): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -120,7 +126,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account") + "payment_account": get_random("Account", filters={'account_currency': currency}), + "currency": currency } if other_details and isinstance(other_details, dict): details.update(other_details) @@ -134,16 +141,16 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True) + create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -151,8 +158,15 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.variable = 5000 salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure + salary_structure_assignment.currency = currency + salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment + +def get_payable_account(company=None): + if not company: + company = erpnext.get_default_company() + return frappe.db.get_value("Company", company, "default_payroll_payable_account") \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 818e853154d..6cd897e95d1 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -6,9 +6,6 @@ frappe.ui.form.on('Salary Structure Assignment', { frm.set_query("employee", function() { return { query: "erpnext.controllers.queries.employee_query", - filters: { - company: frm.doc.company - } } }); frm.set_query("salary_structure", function() { @@ -26,11 +23,25 @@ frappe.ui.form.on('Salary Structure Assignment', { filters: { company: frm.doc.company, docstatus: 1, - disabled: 0 + disabled: 0, + currency: frm.doc.currency + } + }; + }); + + frm.set_query("payroll_payable_account", function() { + var company_currency = erpnext.get_currency(frm.doc.company); + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + "account_currency": ["in", [frm.doc.currency, company_currency]], } } }); }, + employee: function(frm) { if(frm.doc.employee){ frappe.call({ @@ -52,5 +63,13 @@ frappe.ui.form.on('Salary Structure Assignment', { else{ frm.set_value("company", null); } + }, + + company: function(frm) { + if (frm.doc.company) { + frappe.db.get_value("Company", frm.doc.company, "default_payroll_payable_account", (r) => { + frm.set_value("payroll_payable_account", r.default_payroll_payable_account); + }); + } } }); diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index c84e034c727..92bb347661e 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -11,11 +11,13 @@ "employee_name", "department", "company", + "payroll_payable_account", "column_break_6", "designation", "salary_structure", "from_date", "income_tax_slab", + "currency", "section_break_7", "base", "column_break_9", @@ -94,7 +96,7 @@ "fieldname": "base", "fieldtype": "Currency", "label": "Base", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_9", @@ -104,7 +106,7 @@ "fieldname": "variable", "fieldtype": "Currency", "label": "Variable", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "amended_from", @@ -116,15 +118,35 @@ "read_only": 1 }, { + "depends_on": "salary_structure", "fieldname": "income_tax_slab", "fieldtype": "Link", "label": "Income Tax Slab", "options": "Income Tax Slab" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "employee", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account" } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 19:58:09.964692", + "modified": "2020-11-30 18:07:48.251311", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index 668e0ec4717..dccb5df1a11 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -13,6 +13,8 @@ class DuplicateAssignment(frappe.ValidationError): pass class SalaryStructureAssignment(Document): def validate(self): self.validate_dates() + self.validate_income_tax_slab() + self.set_payroll_payable_account() def validate_dates(self): joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, @@ -31,6 +33,24 @@ class SalaryStructureAssignment(Document): frappe.throw(_("From Date {0} cannot be after employee's relieving Date {1}") .format(self.from_date, relieving_date)) + def validate_income_tax_slab(self): + if not self.income_tax_slab: + return + + income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') + if self.currency != income_tax_slab_currency: + frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) + + def set_payroll_payable_account(self): + if not self.payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value( + "Account", { + "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( + "Company", self.company, "default_currency"), "is_group": 0}) + self.payroll_payable_account = payroll_payable_account + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: return None @@ -43,3 +63,10 @@ def get_assigned_salary_structure(employee, on_date): 'on_date': on_date, }) return salary_structure[0][0] if salary_structure else None + +@frappe.whitelist() +def get_employee_currency(employee): + employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') + if not employee_currency: + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) + return employee_currency \ No newline at end of file diff --git a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json index 94eda4c043a..65d3824f3aa 100644 --- a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json +++ b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json @@ -19,13 +19,15 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "From Amount", + "options": "currency", "reqd": 1 }, { "fieldname": "to_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "To Amount" + "label": "To Amount", + "options": "currency" }, { "default": "0", @@ -53,7 +55,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 18:16:07.596493", + "modified": "2020-10-19 13:44:39.549337", "modified_by": "Administrator", "module": "Payroll", "name": "Taxable Salary Slab", diff --git a/erpnext/payroll/report/salary_register/salary_register.js b/erpnext/payroll/report/salary_register/salary_register.js index 885e3d13c7f..eb4acb91a73 100644 --- a/erpnext/payroll/report/salary_register/salary_register.js +++ b/erpnext/payroll/report/salary_register/salary_register.js @@ -8,34 +8,48 @@ frappe.query_reports["Salary Register"] = { "label": __("From"), "fieldtype": "Date", "default": frappe.datetime.add_months(frappe.datetime.get_today(),-1), - "reqd": 1 + "reqd": 1, + "width": "100px" }, { "fieldname":"to_date", "label": __("To"), "fieldtype": "Date", "default": frappe.datetime.get_today(), - "reqd": 1 + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "options": "Currency", + "label": __("Currency"), + "default": erpnext.get_currency(frappe.defaults.get_default("Company")), + "width": "50px" }, { "fieldname":"employee", "label": __("Employee"), "fieldtype": "Link", - "options": "Employee" + "options": "Employee", + "width": "100px" }, { "fieldname":"company", "label": __("Company"), "fieldtype": "Link", "options": "Company", - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "width": "100px", + "reqd": 1 }, { "fieldname":"docstatus", "label":__("Document Status"), "fieldtype":"Select", "options":["Draft", "Submitted", "Cancelled"], - "default":"Submitted" + "default": "Submitted", + "width": "100px" } ] } diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py index 87010855fdb..a1b1a8c56b5 100644 --- a/erpnext/payroll/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -2,18 +2,22 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt from frappe import _ def execute(filters=None): if not filters: filters = {} - salary_slips = get_salary_slips(filters) + currency = None + if filters.get('currency'): + currency = filters.get('currency') + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) if not salary_slips: return [], [] columns, earning_types, ded_types = get_columns(salary_slips) - ss_earning_map = get_ss_earning_map(salary_slips) - ss_ded_map = get_ss_ded_map(salary_slips) + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips,currency, company_currency) doj_map = get_employee_doj_map() data = [] @@ -21,24 +25,30 @@ def execute(filters=None): row = [ss.name, ss.employee, ss.employee_name, doj_map.get(ss.employee), ss.branch, ss.department, ss.designation, ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] - if not ss.branch == None:columns[3] = columns[3].replace('-1','120') - if not ss.department == None: columns[4] = columns[4].replace('-1','120') - if not ss.designation == None: columns[5] = columns[5].replace('-1','120') - if not ss.leave_without_pay == None: columns[9] = columns[9].replace('-1','130') + if ss.branch is not None: columns[3] = columns[3].replace('-1','120') + if ss.department is not None: columns[4] = columns[4].replace('-1','120') + if ss.designation is not None: columns[5] = columns[5].replace('-1','120') + if ss.leave_without_pay is not None: columns[9] = columns[9].replace('-1','130') for e in earning_types: row.append(ss_earning_map.get(ss.name, {}).get(e)) - row += [ss.gross_pay] + if currency == company_currency: + row += [flt(ss.gross_pay) * flt(ss.exchange_rate)] + else: + row += [ss.gross_pay] for d in ded_types: row.append(ss_ded_map.get(ss.name, {}).get(d)) row.append(ss.total_loan_repayment) - row += [ss.total_deduction, ss.net_pay] - + if currency == company_currency: + row += [flt(ss.total_deduction) * flt(ss.exchange_rate), flt(ss.net_pay) * flt(ss.exchange_rate)] + else: + row += [ss.total_deduction, ss.net_pay] + row.append(currency or company_currency) data.append(row) return columns, data @@ -46,10 +56,19 @@ def execute(filters=None): def get_columns(salary_slips): """ columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150",_("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", - _("Date of Joining") + "::80", _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:130", _("Payment Days") + ":Float:120" + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" ] """ columns = [ @@ -73,15 +92,15 @@ def get_columns(salary_slips): return columns, salary_components[_("Earning")], salary_components[_("Deduction")] -def get_salary_slips(filters): +def get_salary_slips(filters, company_currency): filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) - conditions, filters = get_conditions(filters) + conditions, filters = get_conditions(filters, company_currency) salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s order by employee""" % conditions, filters, as_dict=1) return salary_slips or [] -def get_conditions(filters): +def get_conditions(filters, company_currency): conditions = "" doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} @@ -92,6 +111,8 @@ def get_conditions(filters): if filters.get("to_date"): conditions += " and end_date <= %(to_date)s" if filters.get("company"): conditions += " and company = %(company)s" if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" return conditions, filters @@ -103,26 +124,32 @@ def get_employee_doj_map(): FROM `tabEmployee` """)) -def get_ss_earning_map(salary_slips): - ss_earnings = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_earning_map(salary_slips, currency, company_currency): + ss_earnings = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_earning_map = {} for d in ss_earnings: ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_earning_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) return ss_earning_map -def get_ss_ded_map(salary_slips): - ss_deductions = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_ded_map(salary_slips, currency, company_currency): + ss_deductions = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_ded_map = {} for d in ss_deductions: ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_ded_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) return ss_ded_map diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269a..2f15cbcef1a 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -49,7 +49,8 @@ "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", "public/js/call_popup/call_popup.js", - "public/js/utils/dimension_tree_filter.js" + "public/js/utils/dimension_tree_filter.js", + "public/js/telephony.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 5e4d4a585fa..aeb3b387f2b 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -74,7 +74,7 @@ class CallPopup { 'click': () => { const call_summary = this.dialog.get_value('call_summary'); if (!call_summary) return; - frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', { + frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { 'call_log': this.call_log.name, 'summary': call_summary, }).then(() => { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58ac38f0a85..db85a3ec99e 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -189,6 +189,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ frappe.model.round_floats_in(item, ["qty", "received_qty"]); item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); + item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); } this._super(doc, cdt, cdn); @@ -218,8 +219,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ var is_negative_qty = false; for(var i = 0; i 0 && qty > 0 && cur_frm.doc.items[i].item_code == d.item_code && !cur_frm.doc.items[i].material_request_item) - { - cur_frm.doc.items[i].material_request = d.mr_name; - cur_frm.doc.items[i].material_request_item = d.mr_item; - var my_qty = Math.min(qty, d.qty); - qty = qty - my_qty; - d.qty = d.qty - my_qty; - cur_frm.doc.items[i].stock_qty = my_qty*cur_frm.doc.items[i].conversion_factor; - cur_frm.doc.items[i].qty = my_qty; - - frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + cur_frm.doc.items[i].idx + ")"); - if (qty > 0) - { - frappe.msgprint("Splitting " + qty + " units of " + d.item_code); - var newrow = frappe.model.add_child(cur_frm.doc, cur_frm.doc.items[i].doctype, "items"); - item_length++; - - for (var key in cur_frm.doc.items[i]) - { - newrow[key] = cur_frm.doc.items[i][key]; - } - - newrow.idx = item_length; - newrow["stock_qty"] = newrow.conversion_factor*qty; - newrow["qty"] = qty; - - newrow["material_request"] = ""; - newrow["material_request_item"] = ""; - - } - } - }); - i++; - } - refresh_field("items"); - //cur_frm.save(); - } - } - }); - }, - update_auto_repeat_reference: function(doc) { if (doc.auto_repeat) { frappe.call({ @@ -422,6 +359,62 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ cur_frm.add_fetch('project', 'cost_center', 'cost_center'); +erpnext.buying.link_to_mrs = function(frm) { + frappe.call({ + method: "erpnext.buying.utils.get_linked_material_requests", + args:{ + items: frm.doc.items.map((item) => item.item_code) + }, + callback: function(r) { + if (!r.message || r.message.length == 0) { + frappe.throw({ + message: __("No pending Material Requests found to link for the given items."), + title: __("Note") + }); + } + + var item_length = frm.doc.items.length; + for (let item of frm.doc.items) { + var qty = item.qty; + (r.message[0] || []).forEach(function(d) { + if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item) + { + item.material_request = d.mr_name; + item.material_request_item = d.mr_item; + var my_qty = Math.min(qty, d.qty); + qty = qty - my_qty; + d.qty = d.qty - my_qty; + item.stock_qty = my_qty*item.conversion_factor; + item.qty = my_qty; + + frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")"); + if (qty > 0) + { + frappe.msgprint("Splitting " + qty + " units of " + d.item_code); + var newrow = frappe.model.add_child(frm.doc, item.doctype, "items"); + item_length++; + + for (var key in item) + { + newrow[key] = item[key]; + } + + newrow.idx = item_length; + newrow["stock_qty"] = newrow.conversion_factor*qty; + newrow["qty"] = qty; + + newrow["material_request"] = ""; + newrow["material_request_item"] = ""; + + } + } + }); + } + refresh_field("items"); + } + }); +} + erpnext.buying.get_default_bom = function(frm) { $.each(frm.doc["items"] || [], function(i, d) { if (d.item_code && d.bom === "") { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 99f3995a662..22e75780b85 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -609,6 +609,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_outstanding_amount(update_paid_amount); }, + is_internal_invoice: function() { + if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { + if (this.frm.doc.company === this.frm.doc.represents_company) { + return true; + } + } + return false; + }, + calculate_outstanding_amount: function(update_paid_amount) { // NOTE: // paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice @@ -617,7 +626,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_paid_amount(); } - if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return; + if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return; frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1358a4bd088..3bc20f87336 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -209,6 +209,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); } + if (this.frm.fields_dict.taxes_and_charges) { + this.frm.set_query("taxes_and_charges", function() { + return { + filters: [ + ['company', '=', me.frm.doc.company], + ['docstatus', '!=', 2] + ] + }; + }); + } + }, onload: function() { var me = this; @@ -397,7 +408,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ show_description(row_to_modify.idx, row_to_modify.item_code); - this.frm.from_barcode = true; + this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1; frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, { item_code: data.item_code, qty: (row_to_modify.qty || 0) + 1 @@ -481,7 +492,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ d.item_code = ""; } - this.frm.from_barcode = true; + this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1; this.item_code(doc, cdt, cdn); }, @@ -498,11 +509,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ show_batch_dialog = 1; } // clear barcode if setting item (else barcode will take priority) - if(!this.frm.from_barcode) { + if (this.frm.from_barcode == 0) { item.barcode = null; } + this.frm.from_barcode = this.frm.from_barcode - 1 >= 0 ? this.frm.from_barcode - 1 : 0; + - this.frm.from_barcode = false; if(item.item_code || item.barcode || item.serial_no) { if(!this.validate_company_and_party()) { this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove(); diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js new file mode 100644 index 00000000000..f9caadeed7f --- /dev/null +++ b/erpnext/public/js/telephony.js @@ -0,0 +1,23 @@ +frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { + make_input() { + this._super(); + if (this.df.options == 'Phone') { + this.setup_phone(); + } + }, + setup_phone() { + if (frappe.phone_call.handler) { + this.$wrapper.find('.control-input') + .append(` + + + + + `) + .find('.phone-btn') + .click(() => { + frappe.phone_call.handler(this.get_value(), this.frm); + }); + } + } +}); diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 787d557e805..68c8a0d4d38 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -192,19 +192,20 @@ class GSTR3BReport(Document): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_type_map.get(d["ty"]) - gst_category = ["Registered Regular"] if d["ty"] == 'ISRC': - reverse_charge = "Y" + reverse_charge = ["Y"] itc_type = 'All Other ITC' gst_category = ['Unregistered', 'Overseas'] else: - reverse_charge = "N" + gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] + reverse_charge = ["N", "Y"] for account_head in self.account_heads: for category in gst_category: - for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: - d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) + for charge_type in reverse_charge: + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2) for key in ['iamt', 'camt', 'samt', 'csamt']: net_itc[key] += flt(d[key], 2) @@ -264,7 +265,8 @@ class GSTR3BReport(Document): def get_itc_details(self): itc_amount = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, + t.account_head, s.eligibility_for_itc, s.reverse_charge from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t where s.docstatus = 1 and t.parent = s.name and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s @@ -387,7 +389,7 @@ class GSTR3BReport(Document): tax_template = 'Purchase Taxes and Charges' tax_amounts = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head from `tab{doctype}` s , `tab{template}` t where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 3c156479c58..87baece65d3 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { tax_category: function(frm) { frm.trigger('get_tax_template'); }, + customer_address: function(frm) { + frm.trigger('get_tax_template'); + }, get_tax_template: function(frm) { if (!frm.doc.company) return; @@ -19,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { 'shipping_address': frm.doc.shipping_address || '', 'shipping_address_name': frm.doc.shipping_address_name || '', 'customer_address': frm.doc.customer_address || '', + 'supplier_address': frm.doc.supplier_address, 'customer': frm.doc.customer, 'supplier': frm.doc.supplier, 'supplier_gstin': frm.doc.supplier_gstin, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 62487ba2aa0..f8520c2d003 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -12,6 +12,7 @@ from erpnext.regional.india import number_state_mapping from six import string_types from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.utils import get_account_currency +from frappe.model.utils import get_fetch_values def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -161,6 +162,8 @@ def get_regional_address_details(party_details, doctype, company): party_details = json.loads(party_details) party_details = frappe._dict(party_details) + update_party_details(party_details, doctype) + party_details.place_of_supply = get_place_of_supply(party_details, doctype) if is_internal_transfer(party_details, doctype): @@ -209,6 +212,11 @@ def get_regional_address_details(party_details, doctype, company): return party_details +def update_party_details(party_details, doctype): + for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']: + if party_details.get(address_field): + party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field))) + def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): destination_gstin = party_details.company_gstin diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 282efe47901..ad3de5f398d 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -78,7 +78,7 @@ class Gstr1Report(object): place_of_supply = invoice_details.get("place_of_supply") ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{ + b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{ "place_of_supply": "", "ecommerce_gstin": "", "rate": "", @@ -90,7 +90,7 @@ class Gstr1Report(object): "invoice_value": invoice_details.get("base_grand_total"), }) - row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) + row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate @@ -151,6 +151,7 @@ class Gstr1Report(object): {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} + and is_opening = 'No' order by posting_date desc """.format(select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions), self.filters, as_dict=1) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0172d9c128f..29214ee06d9 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -58,6 +58,7 @@ class Customer(TransactionBase): self.set_loyalty_program() self.check_customer_group_change() self.validate_default_bank_account() + self.validate_internal_customer() # set loyalty program tier if frappe.db.exists('Customer', self.name): @@ -82,6 +83,11 @@ class Customer(TransactionBase): if not is_company_account: frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + def validate_internal_customer(self): + if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company))) + def on_update(self): self.validate_name_with_customer_group() self.create_primary_contact() @@ -398,7 +404,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, # form a list of emails and names to show to the user credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) + frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) message = """Please contact any of the following users to extend the credit limits for {0}:

""".format(customer, '
  • '.join(credit_controller_users_formatted)) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 73cc0b836e4..d4fb07cc275 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Sales Order", { frm.custom_make_buttons = { 'Delivery Note': 'Delivery Note', 'Pick List': 'Pick List', - 'Sales Invoice': 'Invoice', + 'Sales Invoice': 'Sales Invoice', 'Material Request': 'Material Request', 'Purchase Order': 'Purchase Order', 'Project': 'Project', @@ -326,8 +326,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( callback: function(r) { if(r.message) { frappe.msgprint({ - message: __('Work Orders Created: {0}', - [r.message.map(function(d) { + message: __('Work Orders Created: {0}', [r.message.map(function(d) { return repl('%(name)s', {name:d}) }).join(', ')]), indicator: 'green' diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 04d85e575cc..accf59ebc45 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -418,8 +418,7 @@ class SalesOrder(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): - delivery_date = get_next_schedule_date(ref_doc_delivery_date, - auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day)) + delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) if delivery_date <= transaction_date: delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 970d8406654..d4cde433590 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -111,24 +111,24 @@ erpnext.PointOfSale.Controller = class { dialog.show(); } - prepare_app_defaults(data) { + async prepare_app_defaults(data) { this.pos_opening = data.name; this.company = data.company; this.pos_profile = data.pos_profile; this.pos_opening_time = data.period_start_date; + this.item_stock_map = {}; + this.settings = {}; frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - this.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.cart.make_customer_selector(); + this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); + this.settings.hide_images = profile.hide_images; + this.settings.auto_add_item_to_cart = profile.auto_add_item_to_cart; + this.make_app(); }); - - this.item_stock_map = {}; - - this.make_app(); } set_opening_entry_status() { @@ -238,12 +238,11 @@ erpnext.PointOfSale.Controller = class { this.item_selector = new erpnext.PointOfSale.ItemSelector({ wrapper: this.$components_wrapper, pos_profile: this.pos_profile, + settings: this.settings, events: { item_selected: args => this.on_cart_update(args), - get_frm: () => this.frm || {}, - - get_allowed_item_group: () => this.item_groups + get_frm: () => this.frm || {} } }) } @@ -251,6 +250,7 @@ erpnext.PointOfSale.Controller = class { init_item_cart() { this.cart = new erpnext.PointOfSale.ItemCart({ wrapper: this.$components_wrapper, + settings: this.settings, events: { get_frm: () => this.frm, @@ -273,9 +273,7 @@ erpnext.PointOfSale.Controller = class { this.customer_details = details; // will add/remove LP payment method this.payment.render_loyalty_points_payment_mode(); - }, - - get_allowed_customer_group: () => this.customer_groups + } } }) } @@ -644,8 +642,7 @@ erpnext.PointOfSale.Controller = class { }) } else if (available_qty < qty_needed) { frappe.show_alert({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', - [bold_item_code, bold_warehouse, bold_available_qty]), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); frappe.utils.play_sound("error"); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7799dacacb1..3938300a2ad 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1,8 +1,10 @@ erpnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events, settings }) { this.wrapper = wrapper; this.events = events; this.customer_info = undefined; + this.hide_images = settings.hide_images; + this.allowed_customer_groups = settings.customer_groups; this.init_component(); } @@ -32,6 +34,7 @@ erpnext.PointOfSale.ItemCart = class { `
    ` ) this.$customer_section = this.$component.find('.customer-section'); + this.make_customer_selector(); } reset_customer_selector() { @@ -302,7 +305,7 @@ erpnext.PointOfSale.ItemCart = class { this.$customer_section.html(`
    `); const me = this; const query = { query: 'erpnext.controllers.queries.customer_query' }; - const allowed_customer_group = this.events.get_allowed_customer_group() || []; + const allowed_customer_group = this.allowed_customer_groups || []; if (allowed_customer_group.length) { query.filters = { customer_group: ['in', allowed_customer_group] @@ -423,6 +426,7 @@ erpnext.PointOfSale.ItemCart = class { } update_customer_section() { + const me = this; const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; if (customer) { @@ -460,7 +464,7 @@ erpnext.PointOfSale.ItemCart = class { } function get_customer_image() { - if (image) { + if (!me.hide_images && image) { return `
    ${image}
    ` diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 49d42814ab0..a06b3942f73 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,8 +1,10 @@ erpnext.PointOfSale.ItemSelector = class { - constructor({ frm, wrapper, events, pos_profile }) { + constructor({ frm, wrapper, events, pos_profile, settings }) { this.wrapper = wrapper; this.events = events; this.pos_profile = pos_profile; + this.hide_images = settings.hide_images; + this.auto_add_item = settings.auto_add_item_to_cart; this.inti_component(); } @@ -26,13 +28,14 @@ erpnext.PointOfSale.ItemSelector = class {
    ALL ITEMS
    -
    +
    ` ); this.$component = this.wrapper.find('.items-selector'); + this.$items_container = this.$component.find('.items-container'); } async load_items_data() { @@ -65,7 +68,6 @@ erpnext.PointOfSale.ItemSelector = class { render_item_list(items) { - this.$items_container = this.$component.find('.items-container'); this.$items_container.html(''); items.forEach(item => { @@ -75,11 +77,12 @@ erpnext.PointOfSale.ItemSelector = class { } get_item_html(item) { + const me = this; const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; function get_item_image_html() { - if (item_image) { + if (!me.hide_images && item_image) { return `
    ${frappe.get_abbr(item.item_name)}
    ` @@ -203,6 +206,7 @@ erpnext.PointOfSale.ItemSelector = class { ignore_inputs: true, page: cur_page.page.page }); + // for selecting the last filtered item on search frappe.ui.keys.on("enter", () => { const selector_is_visible = this.$component.is(':visible'); @@ -235,6 +239,7 @@ erpnext.PointOfSale.ItemSelector = class { const items = this.search_index[search_term]; this.items = items; this.render_item_list(items); + this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); return; } } @@ -247,8 +252,13 @@ erpnext.PointOfSale.ItemSelector = class { } this.items = items; this.render_item_list(items); + this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); }); } + + add_filtered_item_to_cart() { + this.$items_container.find(".item-wrapper").click(); + } resize_selector(minimize) { minimize ? diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index c716aa96e0a..84732760019 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -10,8 +10,8 @@ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): filters = frappe._dict(filters or {}) if filters.from_date > filters.to_date: - frappe.throw(_('From Date cannot be greater than To Date')) - + frappe.throw(_("From Date cannot be greater than To Date")) + columns = get_columns(filters) data = get_data(filters) @@ -148,14 +148,16 @@ def get_data(filters): company_list.append(filters.get("company")) customer_details = get_customer_details() + item_details = get_item_details() sales_order_records = get_sales_order_details(company_list, filters) for record in sales_order_records: customer_record = customer_details.get(record.customer) + item_record = item_details.get(record.item_code) row = { "item_code": record.item_code, - "item_name": record.item_name, - "item_group": record.item_group, + "item_name": item_record.item_name, + "item_group": item_record.item_group, "description": record.description, "quantity": record.qty, "uom": record.uom, @@ -196,8 +198,8 @@ def get_conditions(filters): return conditions def get_customer_details(): - details = frappe.get_all('Customer', - fields=['name', 'customer_name', "customer_group"]) + details = frappe.get_all("Customer", + fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: customer_details.setdefault(d.name, frappe._dict({ @@ -206,15 +208,25 @@ def get_customer_details(): })) return customer_details +def get_item_details(): + details = frappe.db.get_all("Item", + fields=["item_code", "item_name", "item_group"]) + item_details = {} + for d in details: + item_details.setdefault(d.item_code, frappe._dict({ + "item_name": d.item_name, + "item_group": d.item_group + })) + return item_details + def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) return frappe.db.sql(""" SELECT - so_item.item_code, so_item.item_name, so_item.item_group, - so_item.description, so_item.qty, so_item.uom, - so_item.base_rate, so_item.base_amount, so.name, - so.transaction_date, so.customer, so.territory, + so_item.item_code, so_item.description, so_item.qty, + so_item.uom, so_item.base_rate, so_item.base_amount, + so.name, so.transaction_date, so.customer,so.territory, so.project, so_item.delivered_qty, so_item.billed_amt, so.company FROM diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 002cfe41e18..7f00fca8f05 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -42,16 +42,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ me.frm.set_query('customer_address', erpnext.queries.address_query); me.frm.set_query('shipping_address_name', erpnext.queries.address_query); - if(this.frm.fields_dict.taxes_and_charges) { - this.frm.set_query("taxes_and_charges", function() { - return { - filters: [ - ['Sales Taxes and Charges Template', 'company', '=', me.frm.doc.company], - ['Sales Taxes and Charges Template', 'docstatus', '!=', 2] - ] - } - }); - } if(this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function() { @@ -479,7 +469,7 @@ frappe.ui.form.on(cur_frm.doctype,"project", function(frm) { $.each(frm.doc["items"] || [], function(i, row) { if(r.message) { frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); - frappe.msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message)); + frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); } }) } diff --git a/erpnext/setup/desk_page/home/home.json b/erpnext/setup/desk_page/home/home.json index 9cf9f419072..0fbd0eccda8 100644 --- a/erpnext/setup/desk_page/home/home.json +++ b/erpnext/setup/desk_page/home/home.json @@ -1,25 +1,5 @@ { "cards": [ - { - "hidden": 0, - "label": "Healthcare", - "links": "[\n {\n \"label\": \"Patient\",\n \"name\": \"Patient\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Diagnosis\",\n \"name\": \"Diagnosis\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Agriculture", - "links": "[\n {\n \"label\": \"Crop\",\n \"name\": \"Crop\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Crop Cycle\",\n \"name\": \"Crop Cycle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Location\",\n \"name\": \"Location\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fertilizer\",\n \"name\": \"Fertilizer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Education", - "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Non Profit", - "links": "[\n {\n \"description\": \"Member information.\",\n \"label\": \"Member\",\n \"name\": \"Member\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Volunteer information.\",\n \"label\": \"Volunteer\",\n \"name\": \"Volunteer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Chapter information.\",\n \"label\": \"Chapter\",\n \"name\": \"Chapter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Donor information.\",\n \"label\": \"Donor\",\n \"name\": \"Donor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, { "hidden": 0, "label": "Stock", @@ -54,10 +34,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Home", - "modified": "2020-05-11 10:20:37.358701", + "modified": "2020-12-07 14:22:38.667767", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -96,4 +77,4 @@ "type": "Page" } ] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index cbf67b4cd6f..36033d9daee 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -274,7 +274,8 @@ erpnext.company.setup_queries = function(frm) { ["default_employee_advance_account", {"root_type": "Asset"}], ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], - ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}] + ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], + ["unrealized_profit_loss_account", {"root_type": "Liability"}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 40938ea0a5e..d49ae7ce8ac 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -46,10 +46,9 @@ "round_off_account", "round_off_cost_center", "write_off_account", - "discount_allowed_account", - "discount_received_account", "exchange_gain_loss_account", "unrealized_exchange_gain_loss_account", + "unrealized_profit_loss_account", "column_break0", "allow_account_creation_against_child_company", "default_payable_account", @@ -261,14 +260,14 @@ { "fieldname": "create_chart_of_accounts_based_on", "fieldtype": "Select", - "label": "Create Chart of Accounts Based on", + "label": "Create Chart Of Accounts Based On", "options": "\nStandard Template\nExisting Company" }, { "depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"", "fieldname": "chart_of_accounts", "fieldtype": "Select", - "label": "Chart of Accounts Template", + "label": "Chart Of Accounts Template", "no_copy": 1 }, { @@ -345,18 +344,6 @@ "label": "Write Off Account", "options": "Account" }, - { - "fieldname": "discount_allowed_account", - "fieldtype": "Link", - "label": "Discount Allowed Account", - "options": "Account" - }, - { - "fieldname": "discount_received_account", - "fieldtype": "Link", - "label": "Discount Received Account", - "options": "Account" - }, { "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", @@ -740,6 +727,12 @@ "fieldtype": "Link", "label": "Default In Transit Warehouse", "options": "Warehouse" + }, + { + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -747,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-08-06 00:38:08.311216", + "modified": "2020-12-03 12:27:27.085094", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -808,4 +801,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index 8f7593d6eef..b71a92f8a98 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -5,8 +5,7 @@ frappe.ui.form.on('Sales Person', { refresh: function(frm) { if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { var info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', - [format_currency(info.allocated_amount, info.currency)]), 'blue'); + frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue'); } }, diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 0ccc0252c31..c2549fe7dd4 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -345,7 +345,7 @@ def _set_price_list(cart_settings, quotation=None): selling_price_list = None # check if default customer price list exists - if party_name: + if party_name and frappe.db.exists("Customer", party_name): selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) # check default price list in shopping cart diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3b..9068e338c30 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-12-02 15:47:41.532942", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 251a26a592e..03921c554e3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -156,6 +156,11 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } if (!doc.is_return && doc.status!="Closed") { + if(doc.docstatus == 1) { + this.frm.add_custom_button(__('Shipment'), function() { + me.make_shipment() }, __('Create')); + } + if(flt(doc.per_installed, 2) < 100 && doc.docstatus==1) this.frm.add_custom_button(__('Installation Note'), function() { me.make_installation_note() }, __('Create')); @@ -220,6 +225,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }, + make_shipment: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_shipment", + frm: this.frm + }) + }, + make_sales_invoice: function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 7393c8a70eb..c9f8d0810e3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -133,6 +133,7 @@ "per_installed", "installation_status", "column_break_89", + "per_returned", "excise_page", "instructions", "subscription_section", @@ -1099,7 +1100,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed", + "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1251,13 +1252,22 @@ "fieldtype": "Link", "label": "Inter Company Reference", "options": "Purchase Receipt" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_returned", + "fieldtype": "Percent", + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-11-11 14:57:16.388139", + "modified": "2020-11-30 12:54:45.407289", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d04cf785ab1..3f3407e3501 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -55,7 +55,7 @@ class DeliveryNote(SellingController): 'no_allowance': 1 }] if cint(self.is_return): - self.status_updater.append({ + self.status_updater.extend([{ 'source_dt': 'Delivery Note Item', 'target_dt': 'Sales Order Item', 'join_field': 'so_detail', @@ -69,7 +69,19 @@ class DeliveryNote(SellingController): where name=`tabDelivery Note Item`.parent and is_return=1)""", 'second_source_extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""" - }) + }, + { + 'source_dt': 'Delivery Note Item', + 'target_dt': 'Delivery Note Item', + 'join_field': 'dn_detail', + 'target_field': 'returned_qty', + 'target_parent_dt': 'Delivery Note', + 'target_parent_field': 'per_returned', + 'target_ref_field': 'stock_qty', + 'source_field': '-1 * stock_qty', + 'percent_join_field_parent': 'return_against' + } + ]) def before_print(self): def toggle_print_hide(meta, fieldname): @@ -569,6 +581,62 @@ def make_packing_slip(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_shipment(source_name, target_doc=None): + def postprocess(source, target): + user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + target.pickup_contact_email = user.email + pickup_contact_display = '{}'.format(user.full_name) + if user: + if user.email: + pickup_contact_display += '
    ' + user.email + if user.phone: + pickup_contact_display += '
    ' + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += '
    ' + user.mobile_no + target.pickup_contact = pickup_contact_display + + contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) + delivery_contact_display = '{}'.format(source.contact_display) + if contact: + if contact.email_id: + delivery_contact_display += '
    ' + contact.email_id + if contact.phone: + delivery_contact_display += '
    ' + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += '
    ' + contact.mobile_no + target.delivery_contact = delivery_contact_display + + doclist = get_mapped_doc("Delivery Note", source_name, { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "address_display": "delivery_address", + "customer": "delivery_customer", + "shipping_address_name": "delivery_address_name", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email" + }, + "validation": { + "docstatus": ["=", 1] + } + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total" + } + } + }, target_doc, postprocess) + + return doclist @frappe.whitelist() def make_sales_return(source_name, target_doc=None): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 0ae7c37b3f8..4a6500cfd8f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -6,9 +6,11 @@ frappe.listview_settings['Delivery Note'] = { return [__("Return"), "darkgrey", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; + } else if (flt(doc.per_returned, 2) === 100) { + return [__("Return Issued"), "grey", "per_returned,=,100"]; } else if (flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; - } else if (flt(doc.per_billed, 2) == 100) { + } else if (flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } }, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9566af7b38e..6b4663a688a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -206,7 +206,7 @@ class TestDeliveryNote(unittest.TestCase): for field, value in field_values.items(): self.assertEqual(cstr(serial_no.get(field)), value) - def test_sales_return_for_non_bundled_items(self): + def test_sales_return_for_non_bundled_items_partial(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) @@ -225,7 +225,10 @@ class TestDeliveryNote(unittest.TestCase): # return entry dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", do_not_submit=1) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1") @@ -243,6 +246,70 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(gle_warehouse_amount, stock_value_difference) + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Delivery Note", dn1.name) + returned.update_prevdoc_status() + dn.load_from_db() + + # Check if Original DN updated + self.assertEqual(dn.items[0].returned_qty, 2) + self.assertEqual(dn.per_returned, 40) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_dn_2 = make_return_doc("Delivery Note", dn.name) + + # Check if unreturned amount is mapped in 2nd return + self.assertEqual(return_dn_2.items[0].qty, -3) + + si = make_sales_invoice(dn.name) + si.submit() + + self.assertEqual(si.items[0].qty, 3) + + dn.load_from_db() + # DN should be completed on billing all unreturned amount + self.assertEqual(dn.items[0].billed_amt, 1500) + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.status, 'Completed') + + si.load_from_db() + si.cancel() + + dn.load_from_db() + self.assertEqual(dn.per_billed, 0) + + dn1.cancel() + dn.cancel() + + def test_sales_return_for_non_bundled_items_full(self): + from erpnext.stock.doctype.item.test_item import make_item + + company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + + make_item("Box", {'is_stock_item': 1}) + + make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100) + + dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company, + expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + + #return entry + dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500, + company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", do_not_submit=1) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() + + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Delivery Note", dn1.name) + returned.update_prevdoc_status() + dn.load_from_db() + + # Check if Original DN updated + self.assertEqual(dn.items[0].returned_qty, 5) + self.assertEqual(dn.per_returned, 100) + self.assertEqual(dn.status, 'Return Issued') + def test_return_single_item_from_bundled_items(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 3d57f476010..7b471874af7 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-04-22 13:15:44", "doctype": "DocType", @@ -24,7 +25,10 @@ "col_break2", "uom", "conversion_factor", + "stock_qty_sec_break", "stock_qty", + "stock_qty_col_break", + "returned_qty", "section_break_17", "price_list_rate", "base_price_list_rate", @@ -211,7 +215,7 @@ { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Qty as per Stock UOM", + "label": "Qty in Stock UOM", "no_copy": 1, "print_hide": 1, "read_only": 1 @@ -715,12 +719,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "stock_qty_sec_break", + "fieldtype": "Section Break" + }, + { + "fieldname": "stock_qty_col_break", + "fieldtype": "Column Break" + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty in Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-07-20 12:25:06.177894", + "modified": "2020-07-31 20:12:43.054342", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 13c8ceb7596..5bb3095708f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -111,6 +111,7 @@ "range", "column_break4", "per_billed", + "per_returned", "is_internal_supplier", "inter_company_reference", "subscription_detail", @@ -895,7 +896,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed", + "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1104,13 +1105,22 @@ "fieldtype": "Small Text", "label": "Billing Address", "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_returned", + "fieldtype": "Percent", + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-10-30 14:00:08.347534", + "modified": "2020-11-30 12:54:23.278500", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 2cc4679c8c6..97e0fa738cd 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -55,20 +55,33 @@ class PurchaseReceipt(BuyingController): 'percent_join_field': 'material_request' }] if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'po_detail', - 'extra_cond': """ and exists (select name from `tabPurchase Receipt` - where name=`tabPurchase Receipt Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" - }) + self.status_updater.extend([ + { + 'source_dt': 'Purchase Receipt Item', + 'target_dt': 'Purchase Order Item', + 'join_field': 'purchase_order_item', + 'target_field': 'returned_qty', + 'source_field': '-1 * qty', + 'second_source_dt': 'Purchase Invoice Item', + 'second_source_field': '-1 * qty', + 'second_join_field': 'po_detail', + 'extra_cond': """ and exists (select name from `tabPurchase Receipt` + where name=`tabPurchase Receipt Item`.parent and is_return=1)""", + 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" + }, + { + 'source_dt': 'Purchase Receipt Item', + 'target_dt': 'Purchase Receipt Item', + 'join_field': 'purchase_receipt_item', + 'target_field': 'returned_qty', + 'target_parent_dt': 'Purchase Receipt', + 'target_parent_field': 'per_returned', + 'target_ref_field': 'received_stock_qty', + 'source_field': '-1 * received_stock_qty', + 'percent_join_field_parent': 'return_against' + } + ]) def validate(self): self.validate_posting_time() @@ -478,7 +491,7 @@ class PurchaseReceipt(BuyingController): frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate)) def update_status(self, status): - self.set_status(update=True, status = status) + self.set_status(update=True, status=status) self.notify_update() clear_doctype_notifications(self) @@ -490,7 +503,7 @@ class PurchaseReceipt(BuyingController): for pr in set(updated_pr): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) - pr_doc.update_billing_percentage(update_modified=update_modified) + update_billing_percentage(pr_doc, update_modified=update_modified) self.load_from_db() @@ -500,7 +513,7 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail) billed_against_po = billed_against_po and billed_against_po[0][0] or 0 - # Get all Delivery Note Item rows against the Sales Order Item row + # Get all Purchase Receipt Item rows against the Purchase Order Item row pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name=pr_item.parent and pr_item.purchase_order_item=%s @@ -530,6 +543,39 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): return updated_pr +def update_billing_percentage(pr_doc, update_modified=True): + # Reload as billed amount was set in db directly + pr_doc.load_from_db() + + # Update Billing % based on pending accepted qty + total_amount, total_billed_amount = 0, 0 + for item in pr_doc.items: + return_data = frappe.db.get_list("Purchase Receipt", + fields = [ + "sum(abs(`tabPurchase Receipt Item`.qty)) as qty" + ], + filters = [ + ["Purchase Receipt", "docstatus", "=", 1], + ["Purchase Receipt", "is_return", "=", 1], + ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name] + ]) + + returned_qty = return_data[0].qty if return_data else 0 + returned_amount = flt(returned_qty) * flt(item.rate) + pending_amount = flt(item.amount) - returned_amount + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + + total_amount += total_billable_amount + total_billed_amount += flt(item.billed_amt) + + percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) + pr_doc.db_set("per_billed", percent_billed) + pr_doc.load_from_db() + + if update_modified: + pr_doc.set_status(update=True) + pr_doc.notify_update() + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc @@ -552,6 +598,7 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) + target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index e81f323a461..c9501a409ab 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -6,9 +6,11 @@ frappe.listview_settings['Purchase Receipt'] = { return [__("Return"), "darkgrey", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; + } else if (flt(doc.per_returned, 2) === 100) { + return [__("Return Issued"), "grey", "per_returned,=,100"]; } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; - } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) == 100) { + } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 253edb02c35..9b8eeed1a12 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -137,7 +137,10 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) def test_purchase_receipt_gl_entry(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", + get_multiple_items = True, get_taxes_and_charges = True) + self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -281,11 +284,15 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse) - def test_purchase_return(self): + def test_purchase_return_partial(self): + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") - - return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2) + return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", + is_return=1, return_against=pr.name, qty=-2, do_not_submit=1) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() # check sle outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", @@ -309,6 +316,60 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_pr_2 = make_return_doc("Purchase Receipt", pr.name) + + # Check if unreturned amount is mapped in 2nd return + self.assertEqual(return_pr_2.items[0].qty, -3) + + # Make PI against unreturned amount + pi = make_purchase_invoice(pr.name) + pi.submit() + + self.assertEqual(pi.items[0].qty, 3) + + pr.load_from_db() + # PR should be completed on billing all unreturned amount + self.assertEqual(pr.items[0].billed_amt, 150) + self.assertEqual(pr.per_billed, 100) + self.assertEqual(pr.status, 'Completed') + + pi.load_from_db() + pi.cancel() + + pr.load_from_db() + self.assertEqual(pr.per_billed, 0) + + return_pr.cancel() + pr.cancel() + + def test_purchase_return_full(self): + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1") + + return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # hack because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 5) + self.assertEqual(pr.per_returned, 100) + self.assertEqual(pr.status, 'Return Issued') def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse @@ -416,6 +477,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr1.per_billed, 100) self.assertEqual(pr1.status, "Completed") + pr2.load_from_db() self.assertEqual(pr2.get("items")[0].billed_amt, 2000) self.assertEqual(pr2.per_billed, 80) self.assertEqual(pr2.status, "To Bill") diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index c1e1f901ba6..84c64aa8f85 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -28,9 +28,13 @@ "uom", "stock_uom", "conversion_factor", - "stock_qty", "retain_sample", "sample_quantity", + "tracking_section", + "received_stock_qty", + "stock_qty", + "col_break_tracking_section", + "returned_qty", "rate_and_amount", "price_list_rate", "discount_percentage", @@ -526,7 +530,7 @@ { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Accepted Qty as per Stock UOM", + "label": "Accepted Qty in Stock UOM", "oldfieldname": "stock_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -834,12 +838,35 @@ "collapsible": 1, "fieldname": "image_column", "fieldtype": "Column Break" + }, + { + "fieldname": "tracking_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_tracking_section", + "fieldtype": "Column Break" + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty in Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "received_stock_qty", + "fieldtype": "Float", + "label": "Received Qty in Stock UOM", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-28 19:01:21.154963", + "modified": "2020-11-02 10:00:38.204294", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/shipment/__init__.py b/erpnext/stock/doctype/shipment/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js new file mode 100644 index 00000000000..5ccb7d2ff64 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -0,0 +1,447 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment', { + address_query: function(frm, link_doctype, link_name, is_your_company_address) { + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: link_doctype, + link_name: link_name, + is_your_company_address: is_your_company_address + } + }; + }, + contact_query: function(frm, link_doctype, link_name) { + return { + query: 'frappe.contacts.doctype.contact.contact.contact_query', + filters: { + link_doctype: link_doctype, + link_name: link_name + } + }; + }, + onload: function(frm) { + frm.set_query("delivery_address_name", () => { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0); + }); + frm.set_query("pickup_address_name", () => { + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0); + }); + frm.set_query("delivery_contact_name", () => { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + return frm.events.contact_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to]); + }); + frm.set_query("pickup_contact_name", () => { + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]); + }); + frm.set_query("delivery_note", "shipment_delivery_note", function() { + let customer = ''; + if (frm.doc.delivery_to_type == "Customer") { + customer = frm.doc.delivery_customer; + } + if (frm.doc.delivery_to_type == "Company") { + customer = frm.doc.delivery_company; + } + if (customer) { + return { + filters: { + customer: customer, + docstatus: 1, + status: ["not in", ["Cancelled"]] + } + }; + } + }); + }, + refresh: function() { + $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); + $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); + }, + before_save: function(frm) { + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; + frm.set_value("delivery_to", frm.doc[delivery_to]); + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; + frm.set_value("pickup", frm.doc[pickup_from]); + }, + set_pickup_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.pickup_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("pickup_address_name", r.name); + }); + }, + set_delivery_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.delivery_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("delivery_address_name", r.name); + }); + }, + pickup_from_type: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + frm.set_value("pickup_company", frappe.defaults.get_default('company')); + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_supplier", ''); + } else { + frm.trigger('clear_pickup_fields'); + } + if (frm.doc.pickup_from_type == 'Customer') { + frm.set_value("pickup_company", ''); + frm.set_value("pickup_supplier", ''); + } + if (frm.doc.pickup_from_type == 'Supplier') { + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_company", ''); + } + }, + delivery_to_type: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + frm.set_value("delivery_company", frappe.defaults.get_default('company')); + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_supplier", ''); + } else { + frm.trigger('clear_delivery_fields'); + } + if (frm.doc.delivery_to_type == 'Customer') { + frm.set_value("delivery_company", ''); + frm.set_value("delivery_supplier", ''); + } + if (frm.doc.delivery_to_type == 'Supplier') { + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_company", ''); + frm.toggle_display("shipment_delivery_note", false); + } else { + frm.toggle_display("shipment_delivery_note", true); + } + }, + delivery_address_name: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true); + } else { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false); + } + }, + pickup_address_name: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true); + } else { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false); + } + }, + get_contact_display: function(frm, contact_name, contact_type) { + frappe.call({ + method: "frappe.contacts.doctype.contact.contact.get_contact_details", + args: { contact: contact_name }, + callback: function(r) { + if (r.message) { + if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { + if (contact_type == 'Delivery') { + frm.set_value('delivery_contact_name', ''); + frm.set_value('delivery_contact', ''); + } else { + frm.set_value('pickup_contact_name', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") + "
    " + __("Please set Email/Phone for the contact") + ` ${contact_name}`); + } + let contact_display = r.message.contact_display; + if (r.message.contact_email) { + contact_display += '
    ' + r.message.contact_email; + } + if (r.message.contact_phone) { + contact_display += '
    ' + r.message.contact_phone; + } + if (r.message.contact_mobile && !r.message.contact_phone) { + contact_display += '
    ' + r.message.contact_mobile; + } + if (contact_type == 'Delivery') { + frm.set_value('delivery_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('delivery_contact_email', r.message.contact_email); + } + } else { + frm.set_value('pickup_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('pickup_contact_email', r.message.contact_email); + } + } + } + } + }); + }, + delivery_contact_name: function(frm) { + if (frm.doc.delivery_contact_name) { + frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery'); + } + }, + pickup_contact_name: function(frm) { + if (frm.doc.pickup_contact_name) { + frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup'); + } + }, + pickup_contact_person: function(frm) { + if (frm.doc.pickup_contact_person) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frm.doc.pickup_contact_person }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
    ${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
    ${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
    ${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } else { + if (frm.doc.pickup_from_type === 'Company') { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frappe.session.user }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
    ${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
    ${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
    ${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } + } + }, + set_company_contact: function(frm, delivery_type) { + frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => { + if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_company', ''); + frm.set_value('delivery_contact', ''); + } else { + frm.set_value('pickup_company', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
    " + __("Please first set Last Name, Email and Phone for the user") + ` ${frappe.session.user}`); + } + let contact_display = r.full_name; + if (r.email) { + contact_display += '
    ' + r.email; + } + if (r.phone) { + contact_display += '
    ' + r.phone; + } + if (r.mobile_no && !r.phone) { + contact_display += '
    ' + r.mobile_no; + } + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact', contact_display); + if (r.email) { + frm.set_value('delivery_contact_email', r.email); + } + } else { + frm.set_value('pickup_contact', contact_display); + if (r.email) { + frm.set_value('pickup_contact_email', r.email); + } + } + }); + frm.set_value('pickup_contact_person', frappe.session.user); + }, + pickup_company: function(frm) { + if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) { + frm.trigger('set_pickup_company_address'); + frm.events.set_company_contact(frm, 'Pickup'); + } + }, + delivery_company: function(frm) { + if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) { + frm.trigger('set_delivery_company_address'); + frm.events.set_company_contact(frm, 'Delivery'); + } + }, + delivery_customer: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_customer) { + frm.events.set_address_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + } + }, + delivery_supplier: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_supplier) { + frm.events.set_address_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + } + }, + pickup_customer: function(frm) { + if (frm.doc.pickup_customer) { + frm.events.set_address_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + } + }, + pickup_supplier: function(frm) { + if (frm.doc.pickup_supplier) { + frm.events.set_address_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + } + }, + set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_address_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if (r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_address_name', r.message); + } else { + frm.set_value('pickup_address_name', r.message); + } + } + } + }); + }, + set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_contact_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if (r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact_name', r.message); + } else { + frm.set_value('pickup_contact_name', r.message); + } + } + } + }); + }, + add_template: function(frm) { + if (frm.doc.parcel_template) { + frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => { + let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template); + let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel"); + row.length = parcel_template.length; + row.width = parcel_template.width; + row.height = parcel_template.height; + row.weight = parcel_template.weight; + frm.refresh_fields("shipment_parcel"); + }); + } + }, + pickup_date: function(frm) { + if (frm.doc.pickup_date < frappe.datetime.get_today()) { + frappe.throw(__("Pickup Date cannot be before this day")); + } + if (frm.doc.pickup_date == frappe.datetime.get_today()) { + var pickup_time = frm.events.get_pickup_time(frm); + frm.set_value("pickup_from", pickup_time); + frm.trigger('set_pickup_to_time'); + } + }, + pickup_from: function(frm) { + var pickup_time = frm.events.get_pickup_time(frm); + if (frm.doc.pickup_from && frm.doc.pickup_date == frappe.datetime.get_today()) { + let current_hour = pickup_time.split(':')[0]; + let current_min = pickup_time.split(':')[1]; + let pickup_hour = frm.doc.pickup_from.split(':')[0]; + let pickup_min = frm.doc.pickup_from.split(':')[1]; + if (pickup_hour < current_hour || (pickup_hour == current_hour && pickup_min < current_min)) { + frm.set_value("pickup_from", pickup_time); + frappe.throw(__("Pickup Time cannot be in the past")); + } + } + frm.trigger('set_pickup_to_time'); + }, + get_pickup_time: function() { + let current_hour = new Date().getHours(); + let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'}); + if (current_min < 30) { + current_min = '30'; + } else { + current_min = '00'; + current_hour = Number(current_hour)+1; + } + let pickup_time = current_hour +':'+ current_min; + return pickup_time; + }, + set_pickup_to_time: function(frm) { + let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5; + let pickup_to_min = frm.doc.pickup_from.split(':')[1]; + let pickup_to = pickup_to_hour +':'+ pickup_to_min; + frm.set_value("pickup_to", pickup_to); + }, + clear_pickup_fields: function(frm) { + let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; + for (let field of fields) { + frm.set_value(field, ''); + } + }, + clear_delivery_fields: function(frm) { + let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"]; + for (let field of fields) { + frm.set_value(field, ''); + } + }, + remove_email_row: function(frm, table, fieldname) { + $.each(frm.doc[table] || [], function(i, detail) { + if (detail.email === fieldname) { + cur_frm.get_field(table).grid.grid_rows[i].remove(); + } + }); + } +}); + +frappe.ui.form.on('Shipment Delivery Note', { + delivery_note: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.delivery_note) { + let row_index = row.idx - 1; + if (validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { + frappe.throw(__("You have entered a duplicate Delivery Note on Row") + ` ${row.idx}. ` + __("Please rectify and try again.")); + } + } + }, + grand_total: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.grand_total) { + var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total); + frm.set_value("value_of_goods", Math.round(value_of_goods)); + frm.refresh_fields("value_of_goods"); + } + }, +}); + +var validate_duplicate = function(frm, table, fieldname, index) { + return ( + table === 'shipment_delivery_note' + ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) + : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)) + ); +}; diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json new file mode 100644 index 00000000000..37a9cc6c02c --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -0,0 +1,471 @@ +{ + "actions": [], + "autoname": "SHIPMENT-.#####", + "creation": "2020-07-09 10:58:52.508703", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "heading_pickup_from", + "pickup_from_type", + "pickup_company", + "pickup_customer", + "pickup_supplier", + "pickup", + "pickup_address_name", + "pickup_address", + "pickup_contact_person", + "pickup_contact_name", + "pickup_contact_email", + "pickup_contact", + "column_break_2", + "heading_delivery_to", + "delivery_to_type", + "delivery_company", + "delivery_customer", + "delivery_supplier", + "delivery_to", + "delivery_address_name", + "delivery_address", + "delivery_contact_name", + "delivery_contact_email", + "delivery_contact", + "parcels_section", + "shipment_parcel", + "parcel_template", + "add_template", + "column_break_28", + "shipment_delivery_note", + "shipment_details_section", + "pallets", + "value_of_goods", + "pickup_date", + "pickup_from", + "pickup_to", + "column_break_36", + "shipment_type", + "pickup_type", + "incoterm", + "description_of_content", + "section_break_40", + "shipment_information_section", + "service_provider", + "shipment_id", + "shipment_amount", + "status", + "tracking_url", + "column_break_55", + "carrier", + "carrier_service", + "awb_number", + "tracking_status", + "tracking_status_info", + "amended_from" + ], + "fields": [ + { + "fieldname": "heading_pickup_from", + "fieldtype": "Heading", + "label": "Pickup from" + }, + { + "default": "Company", + "fieldname": "pickup_from_type", + "fieldtype": "Select", + "label": "Pickup from", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Company'", + "fieldname": "pickup_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Customer'", + "fieldname": "pickup_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Supplier'", + "fieldname": "pickup_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "pickup", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Pickup From", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"", + "fieldname": "pickup_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "pickup_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type !== \"Company\"", + "fieldname": "pickup_contact_name", + "fieldtype": "Link", + "label": "Contact", + "mandatory_depends_on": "eval: doc.pickup_from_type !== 'Company'", + "options": "Contact" + }, + { + "fieldname": "pickup_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "fieldname": "pickup_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "heading_delivery_to", + "fieldtype": "Heading", + "label": "Delivery to" + }, + { + "default": "Customer", + "fieldname": "delivery_to_type", + "fieldtype": "Select", + "label": "Delivery to", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Company'", + "fieldname": "delivery_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Customer'", + "fieldname": "delivery_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Supplier'", + "fieldname": "delivery_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "delivery_to", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Delivery To", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "delivery_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_contact_name", + "fieldtype": "Link", + "label": "Contact", + "mandatory_depends_on": "eval: doc.delivery_from_type !== 'Company'", + "options": "Contact" + }, + { + "fieldname": "delivery_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "depends_on": "eval:doc.delivery_contact_name", + "fieldname": "delivery_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "parcels_section", + "fieldtype": "Section Break", + "label": "Parcels" + }, + { + "fieldname": "shipment_parcel", + "fieldtype": "Table", + "label": "Shipment Parcel", + "options": "Shipment Parcel" + }, + { + "fieldname": "parcel_template", + "fieldtype": "Link", + "label": "Parcel Template", + "options": "Shipment Parcel Template" + }, + { + "depends_on": "eval:doc.docstatus !== 1\n", + "fieldname": "add_template", + "fieldtype": "Button", + "label": "Add Template" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipment_details_section", + "fieldtype": "Section Break", + "label": "Shipment details" + }, + { + "default": "No", + "fieldname": "pallets", + "fieldtype": "Select", + "label": "Pallets", + "options": "No\nYes" + }, + { + "fieldname": "value_of_goods", + "fieldtype": "Currency", + "label": "Value of Goods", + "precision": "2", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "pickup_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Pickup Date", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "09:00", + "fieldname": "pickup_from", + "fieldtype": "Time", + "label": "Pickup from" + }, + { + "allow_on_submit": 1, + "default": "17:00", + "fieldname": "pickup_to", + "fieldtype": "Time", + "label": "Pickup to" + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "default": "Goods", + "fieldname": "shipment_type", + "fieldtype": "Select", + "label": "Shipment Type", + "options": "Goods\nDocuments" + }, + { + "default": "Pickup", + "fieldname": "pickup_type", + "fieldtype": "Select", + "label": "Pickup Type", + "options": "Pickup\nSelf delivery" + }, + { + "fieldname": "description_of_content", + "fieldtype": "Small Text", + "label": "Description of Content", + "reqd": 1 + }, + { + "fieldname": "section_break_40", + "fieldtype": "Section Break" + }, + { + "fieldname": "shipment_information_section", + "fieldtype": "Section Break", + "label": "Shipment Information" + }, + { + "fieldname": "service_provider", + "fieldtype": "Data", + "label": "Service Provider", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "shipment_id", + "fieldtype": "Data", + "label": "Shipment ID", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "shipment_amount", + "fieldtype": "Currency", + "label": "Shipment Amount", + "no_copy": 1, + "precision": "2", + "print_hide": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", + "print_hide": 1 + }, + { + "fieldname": "tracking_url", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Tracking URL", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "carrier", + "fieldtype": "Data", + "label": "Carrier", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "carrier_service", + "fieldtype": "Data", + "label": "Carrier Service", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "awb_number", + "fieldtype": "Data", + "label": "AWB Number", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "tracking_status", + "fieldtype": "Select", + "label": "Tracking Status", + "no_copy": 1, + "options": "\nIn Progress\nDelivered\nReturned\nLost", + "print_hide": 1 + }, + { + "fieldname": "tracking_status_info", + "fieldtype": "Data", + "label": "Tracking Status Info", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Shipment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Select", + "label": "Incoterm", + "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)" + }, + { + "fieldname": "shipment_delivery_note", + "fieldtype": "Table", + "label": "Shipment Delivery Note", + "options": "Shipment Delivery Note" + }, + { + "depends_on": "eval:doc.pickup_from_type === 'Company'", + "fieldname": "pickup_contact_person", + "fieldtype": "Link", + "label": "Pickup Contact Person", + "mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'", + "options": "User" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-12-02 15:43:44.607039", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py new file mode 100644 index 00000000000..de0c243b057 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -0,0 +1,63 @@ +# -*- 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 import _ +from frappe.utils import flt +from frappe.model.document import Document +from erpnext.accounts.party import get_party_shipping_address +from frappe.contacts.doctype.contact.contact import get_default_contact + +class Shipment(Document): + def validate(self): + self.validate_weight() + self.set_value_of_goods() + if self.docstatus == 0: + self.status = 'Draft' + + def on_submit(self): + if not self.shipment_parcel: + frappe.throw(_('Please enter Shipment Parcel information')) + if self.value_of_goods == 0: + frappe.throw(_('Value of goods cannot be 0')) + self.status = 'Submitted' + + def on_cancel(self): + self.status = 'Cancelled' + + def validate_weight(self): + for parcel in self.shipment_parcel: + if flt(parcel.weight) <= 0: + frappe.throw(_('Parcel weight cannot be 0')) + + def set_value_of_goods(self): + value_of_goods = 0 + for entry in self.get("shipment_delivery_note"): + value_of_goods += flt(entry.get("grand_total")) + self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + +@frappe.whitelist() +def get_address_name(ref_doctype, docname): + # Return address name + return get_party_shipping_address(ref_doctype, docname) + +@frappe.whitelist() +def get_contact_name(ref_doctype, docname): + # Return address name + return get_default_contact(ref_doctype, docname) + +@frappe.whitelist() +def get_company_contact(user): + contact = frappe.db.get_value('User', user, [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.phone: + contact.phone = contact.mobile_no + return contact diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js new file mode 100644 index 00000000000..52b052c81f3 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings['Shipment'] = { + add_fields: ["status"], + get_indicator: function(doc) { + if (doc.status=='Booked') { + return [__("Booked"), "green"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py new file mode 100644 index 00000000000..e1fa207a216 --- /dev/null +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +from datetime import date, timedelta + +import frappe +import unittest +from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment + +class TestShipment(unittest.TestCase): + def test_shipment_from_delivery_note(self): + delivery_note = create_test_delivery_note() + delivery_note.submit() + shipment = create_test_shipment([ delivery_note ]) + shipment.submit() + second_shipment = make_shipment(delivery_note.name) + self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) + self.assertEqual(len(second_shipment.shipment_delivery_note), 1) + self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) + +def create_test_delivery_note(): + company = get_shipment_company() + customer = get_shipment_customer() + item = get_shipment_item(company.name) + posting_date = date.today() + timedelta(days=1) + + create_material_receipt(item, company.name) + delivery_note = frappe.new_doc("Delivery Note") + delivery_note.company = company.name + delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") + delivery_note.posting_time = '10:00' + delivery_note.customer = customer.name + delivery_note.append('items', + { + "item_code": item.name, + "item_name": item.item_name, + "description": 'Test delivery note for shipment', + "qty": 5, + "uom": 'Nos', + "warehouse": 'Stores - SC', + "rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + delivery_note.insert() + frappe.db.commit() + return delivery_note + + +def create_test_shipment(delivery_notes = None): + company = get_shipment_company() + company_address = get_shipment_company_address(company.name) + customer = get_shipment_customer() + customer_address = get_shipment_customer_address(customer.name) + customer_contact = get_shipment_customer_contact(customer.name) + posting_date = date.today() + timedelta(days=5) + + shipment = frappe.new_doc("Shipment") + shipment.pickup_from_type = 'Company' + shipment.pickup_company = company.name + shipment.pickup_address_name = company_address.name + shipment.delivery_to_type = 'Customer' + shipment.delivery_customer = customer.name + shipment.delivery_address_name = customer_address.name + shipment.delivery_contact_name = customer_contact.name + shipment.pallets = 'No' + shipment.shipment_type = 'Goods' + shipment.value_of_goods = 1000 + shipment.pickup_type = 'Pickup' + shipment.pickup_date = posting_date.strftime("%Y-%m-%d") + shipment.pickup_from = '09:00' + shipment.pickup_to = '17:00' + shipment.description_of_content = 'unit test entry' + for delivery_note in delivery_notes: + shipment.append('shipment_delivery_note', + { + "delivery_note": delivery_note.name + } + ) + shipment.append('shipment_parcel', + { + "length": 5, + "width": 5, + "height": 5, + "weight": 5, + "count": 5 + } + ) + shipment.insert() + frappe.db.commit() + return shipment + + +def get_shipment_customer_contact(customer_name): + contact_fname = 'Customer Shipment' + contact_lname = 'Testing' + customer_name = contact_fname + ' ' + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + if len(contacts): + return contacts[0] + else: + return create_customer_contact(contact_fname, contact_lname) + + +def get_shipment_customer_address(customer_name): + address_title = customer_name + ' address 123' + customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(customer_address): + return customer_address[0] + else: + return create_shipment_address(address_title, customer_name, 81929) + +def get_shipment_customer(): + customer_name = 'Shipment Customer' + customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + if len(customer): + return customer[0] + else: + return create_shipment_customer(customer_name) + +def get_shipment_company_address(company_name): + address_title = company_name + ' address 123' + addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(addresses): + return addresses[0] + else: + return create_shipment_address(address_title, company_name, 80331) + +def get_shipment_company(): + company_name = 'Shipment Company' + abbr = 'SC' + companies = frappe.get_all("Company", fields=["name"], filters = {"company_name": company_name}) + if len(companies): + return companies[0] + else: + return create_shipment_company(company_name, abbr) + +def get_shipment_item(company_name): + item_name = 'Testing Shipment item' + items = frappe.get_all("Item", + fields=["name", "item_name", "item_code", "standard_rate"], + filters = {"item_name": item_name} + ) + if len(items): + return items[0] + else: + return create_shipment_item(item_name, company_name) + +def create_shipment_address(address_title, company_name, postal_code): + address = frappe.new_doc("Address") + address.address_title = address_title + address.address_type = 'Shipping' + address.address_line1 = company_name + ' address line 1' + address.city = 'Random City' + address.postal_code = postal_code + address.country = 'Germany' + address.insert() + return address + + +def create_customer_contact(fname, lname): + customer = frappe.new_doc("Contact") + customer.customer_name = fname + ' ' + lname + customer.first_name = fname + customer.last_name = lname + customer.is_primary_contact = 1 + customer.is_billing_contact = 1 + customer.append('email_ids', + { + 'email_id': 'randomme@email.com', + 'is_primary': 1 + } + ) + customer.append('phone_nos', + { + 'phone': '123123123', + 'is_primary_phone': 1, + 'is_primary_mobile_no': 1 + } + ) + customer.status = 'Passive' + customer.insert() + return customer + + +def create_shipment_company(company_name, abbr): + company = frappe.new_doc("Company") + company.company_name = company_name + company.abbr = abbr + company.default_currency = 'EUR' + company.country = 'Germany' + company.insert() + return company + +def create_shipment_customer(customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.customer_type = 'Company' + customer.customer_group = 'All Customer Groups' + customer.territory = 'All Territories' + customer.gst_category = 'Unregistered' + customer.insert() + return customer + +def create_material_receipt(item, company): + posting_date = date.today() + stock = frappe.new_doc("Stock Entry") + stock.company = company + stock.stock_entry_type = 'Material Receipt' + stock.posting_date = posting_date.strftime("%Y-%m-%d") + stock.append('items', + { + "t_warehouse": 'Stores - SC', + "item_code": item.name, + "qty": 5, + "uom": 'Nos', + "basic_rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + stock.insert() + stock.submit() + + +def create_shipment_item(item_name, company_name): + item = frappe.new_doc("Item") + item.item_name = item_name + item.item_code = item_name + item.item_group = 'All Item Groups' + item.stock_uom = 'Nos' + item.standard_rate = 50 + item.append('item_defaults', + { + "company": company_name, + "default_warehouse": 'Stores - SC' + } + ) + item.insert() + return item diff --git a/erpnext/stock/doctype/shipment_delivery_note/__init__.py b/erpnext/stock/doctype/shipment_delivery_note/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json new file mode 100644 index 00000000000..86259137186 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-07-09 11:52:57.939021", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "delivery_note", + "grand_total" + ], + "fields": [ + { + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "options": "Delivery Note", + "reqd": 1 + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Value", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-12-02 15:44:34.028703", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Delivery Note", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py new file mode 100644 index 00000000000..43421516057 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py @@ -0,0 +1,10 @@ +# -*- 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 + +class ShipmentDeliveryNote(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel/__init__.py b/erpnext/stock/doctype/shipment_parcel/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json new file mode 100644 index 00000000000..6943edcdc91 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "creation": "2020-07-09 11:28:48.887737", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "length", + "width", + "height", + "weight", + "count" + ], + "fields": [ + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Count", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:54:14.847170", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py new file mode 100644 index 00000000000..53e6ed55dd3 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py @@ -0,0 +1,10 @@ +# -*- 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 + +class ShipmentParcel(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/__init__.py b/erpnext/stock/doctype/shipment_parcel_template/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js new file mode 100644 index 00000000000..785a3b304de --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment Parcel Template', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json new file mode 100644 index 00000000000..4735d9f8866 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "autoname": "field:parcel_template_name", + "creation": "2020-07-09 11:43:43.470339", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parcel_template_name", + "length", + "width", + "height", + "weight" + ], + "fields": [ + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + }, + { + "fieldname": "parcel_template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parcel Template Name", + "reqd": 1, + "unique": 1 + } + ], + "links": [], + "modified": "2020-09-28 12:51:00.320421", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py new file mode 100644 index 00000000000..2a8d58d8305 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- 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 + +class ShipmentParcelTemplate(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py new file mode 100644 index 00000000000..6e2caa768bf --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestShipmentParcelTemplate(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca4..27fcbb7e2a5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -841,6 +841,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } }, + fg_completed_qty: function() { + this.get_items(); + }, + get_items: function() { var me = this; if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) @@ -850,6 +854,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ // if work order / bom is mentioned, get items return this.frm.call({ doc: me.frm.doc, + freeze: true, method: "get_items", callback: function(r) { if(!r.exc) refresh_field("items"); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3159b95c30..32d7e6eb34c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -120,6 +120,7 @@ class StockEntry(StockController): self.update_transferred_qty() self.update_quality_inspection() self.delete_auto_created_batches() + self.delete_linked_stock_entry() if self.purpose == 'Material Transfer' and self.add_to_transit: self.set_material_request_transfer_status('Not Started') @@ -152,6 +153,12 @@ class StockEntry(StockController): frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") .format(self.job_card)) + def delete_linked_stock_entry(self): + if self.purpose == "Send to Warehouse": + for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, + "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + frappe.delete_doc("Stock Entry", d.name) + def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): @@ -1033,26 +1040,22 @@ class StockEntry(StockController): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', filters={'parent': self.work_order}, - fields=["item_code", "required_qty", "consumed_qty"] + fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"] ) + work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: - qty = item.required_qty - item_account_details = get_item_defaults(item.item_code, self.company) # Take into account consumption if there are any. - if self.purpose == 'Manufacture': - req_qty_each = flt(item.required_qty / wo.qty) - if (flt(item.consumed_qty) != 0): - remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each) - exhaust_qty = req_qty_each * wo.produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 - else: - qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty - else: - qty = req_qty_each * flt(self.fg_completed_qty) + + wo_item_qty = item.transferred_qty or item.required_qty + + req_qty_each = ( + (flt(wo_item_qty) - flt(item.consumed_qty)) / + (flt(work_order_qty) - flt(wo.produced_qty)) + ) + + qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: self.add_to_stock_entry_detail({ @@ -1134,13 +1137,15 @@ class StockEntry(StockController): else: qty = req_qty_each * flt(self.fg_completed_qty) - elif backflushed_materials.get(item.item_code): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): qty = (qty/trans_qty) * flt(self.fg_completed_qty) + if consumed_qty: + qty -= consumed_qty + if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): qty = frappe.utils.ceil(qty) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 4c7828b8737..3b9608b8056 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -55,7 +55,7 @@ class StockSettings(Document): """) if sle: - frappe.throw(_("Can't change valuation method, as there are transactions against some items which does not have it's own valuation method")) + frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method")) def validate_clean_description_html(self): if int(self.clean_description_html or 0) \ diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 1339d9b6820..ccd01001bb7 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items): select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, - sle.item_code as name, sle.voucher_no + sle.item_code as name, sle.voucher_no, sle.stock_value from `tabStock Ledger Entry` sle force index (posting_sort_index) where sle.docstatus < 2 %s %s @@ -197,7 +197,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value_difference) + value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index fe01d4b983c..086755be516 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,6 +1,13 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; + frm.set_query("customer", function () { + return { + filters: { + "disabled": 0 + } + }; + }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { @@ -21,14 +28,14 @@ frappe.ui.form.on("Issue", { }, callback: function (r) { if (r && r.message) { - frm.set_query('priority', function() { + frm.set_query("priority", function() { return { filters: { "name": ["in", r.message.priority], } }; }); - frm.set_query('service_level_agreement', function() { + frm.set_query("service_level_agreement", function() { return { filters: { "name": ["in", r.message.service_level_agreements], @@ -45,9 +52,9 @@ frappe.ui.form.on("Issue", { if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.service_level_agreement) { frappe.call({ - 'method': 'frappe.client.get', + "method": "frappe.client.get", args: { - doctype: 'Service Level Agreement', + doctype: "Service Level Agreement", name: frm.doc.service_level_agreement }, callback: function(data) { @@ -127,8 +134,8 @@ frappe.ui.form.on("Issue", { reset_sla.clear(); frappe.show_alert({ - indicator: 'green', - message: __('Resetting Service Level Agreement.') + indicator: "green", + message: __("Resetting Service Level Agreement.") }); frm.call("reset_service_level_agreement", { @@ -145,35 +152,36 @@ frappe.ui.form.on("Issue", { reset_sla.show(); }, + timeline_refresh: function(frm) { // create button for "Help Article" - if(frappe.model.can_create('Help Article')) { + if (frappe.model.can_create("Help Article")) { // Removing Help Article button if exists to avoid multiple occurance frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove(); $('') .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) - .on('click', function() { - var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html(); - var doc = frappe.model.get_new_doc('Help Article'); + .on("click", function() { + var content = $(this).parents(".timeline-item:first").find(".timeline-item-content").html(); + var doc = frappe.model.get_new_doc("Help Article"); doc.title = frm.doc.subject; doc.content = content; - frappe.set_route('Form', 'Help Article', doc.name); + frappe.set_route("Form", "Help Article", doc.name); }); } - if (!frm.timeline.wrapper.find('.btn-split-issue').length) { + if (!frm.timeline.wrapper.find(".btn-split-issue").length) { let split_issue = __("Split Issue") $(``) .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) if (!frm.timeline.wrapper.data("split-issue-event-attached")){ - frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => { + frm.timeline.wrapper.on("click", ".btn-split-issue", (e) => { var dialog = new frappe.ui.Dialog({ title: __("Split Issue"), fields: [ - {fieldname: 'subject', fieldtype: 'Data', reqd:1, label: __('Subject'), description: __('All communications including and above this shall be moved into the new Issue')} + {fieldname: "subject", fieldtype: "Data", reqd: 1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")} ], primary_action_label: __("Split"), primary_action: function() { @@ -226,7 +234,7 @@ function set_time_to_resolve_and_response(frm) { function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(moment()); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; + let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green"; return {"diff_display": diff_display, "indicator": indicator}; } diff --git a/erpnext/telephony/__init__.py b/erpnext/telephony/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/__init__.py b/erpnext/telephony/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/call_log/__init__.py b/erpnext/telephony/doctype/call_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/call_log/call_log.js b/erpnext/telephony/doctype/call_log/call_log.js new file mode 100644 index 00000000000..977f86da0dd --- /dev/null +++ b/erpnext/telephony/doctype/call_log/call_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Call Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json similarity index 97% rename from erpnext/communication/doctype/call_log/call_log.json rename to erpnext/telephony/doctype/call_log/call_log.json index 31e79f17cd2..55ad2baefdb 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -137,12 +137,11 @@ "read_only": 1 } ], - "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-25 17:08:34.085731", + "modified": "2020-11-25 14:32:44.407815", "modified_by": "Administrator", - "module": "Communication", + "module": "Telephony", "name": "Call Log", "owner": "Administrator", "permissions": [ diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py similarity index 100% rename from erpnext/communication/doctype/call_log/call_log.py rename to erpnext/telephony/doctype/call_log/call_log.py diff --git a/erpnext/telephony/doctype/call_log/test_call_log.py b/erpnext/telephony/doctype/call_log/test_call_log.py new file mode 100644 index 00000000000..faa63041ba3 --- /dev/null +++ b/erpnext/telephony/doctype/call_log/test_call_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestCallLog(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json new file mode 100644 index 00000000000..6d46b4e2cdb --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2020-11-19 11:15:54.967710", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day_of_week", + "from_time", + "to_time", + "agent_group" + ], + "fields": [ + { + "fieldname": "day_of_week", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day Of Week", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + }, + { + "default": "9:00:00", + "fieldname": "from_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "default": "17:00:00", + "fieldname": "to_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "agent_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Agent Group", + "options": "Employee Group", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-19 11:15:54.967710", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Handling Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py new file mode 100644 index 00000000000..fcf29745e2b --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py @@ -0,0 +1,10 @@ +# -*- 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 + +class IncomingCallHandlingSchedule(Document): + pass diff --git a/erpnext/telephony/doctype/incoming_call_settings/__init__.py b/erpnext/telephony/doctype/incoming_call_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js new file mode 100644 index 00000000000..1bcc8461323 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js @@ -0,0 +1,102 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function time_to_seconds(time_str) { + // Convert time string of format HH:MM:SS into seconds. + let seq = time_str.split(':'); + seq = seq.map((n) => parseInt(n)); + return (seq[0]*60*60) + (seq[1]*60) + seq[2]; +} + +function number_sort(array, ascending=true) { + let array_copy = [...array]; + if (ascending) { + array_copy.sort((a, b) => a-b); // ascending order + } else { + array_copy.sort((a, b) => b-a); // descending order + } + return array_copy; +} + +function groupby(items, key) { + // Group the list of items using the given key. + const obj = {}; + items.forEach((item) => { + if (item[key] in obj) { + obj[item[key]].push(item); + } else { + obj[item[key]] = [item]; + } + }); + return obj; +} + +function check_timeslot_overlap(ts1, ts2) { + /// Timeslot is a an array of length 2 ex: [from_time, to_time] + /// time in timeslot is an integer represents number of seconds. + if ((ts1[0] < ts2[0] && ts1[1] <= ts2[0]) || (ts1[0] >= ts2[1] && ts1[1] > ts2[1])) { + return false; + } + return true; +} + +function validate_call_schedule(schedule) { + validate_call_schedule_timeslot(schedule); + validate_call_schedule_overlaps(schedule); +} + +function validate_call_schedule_timeslot(schedule) { + // Make sure that to time slot is ahead of from time slot. + let errors = []; + + for (let row in schedule) { + let record = schedule[row]; + let from_time_in_secs = time_to_seconds(record.from_time); + let to_time_in_secs = time_to_seconds(record.to_time); + if (from_time_in_secs >= to_time_in_secs) { + errors.push(__('Call Schedule Row {0}: To time slot should always be ahead of From time slot.', [row])); + } + } + + if (errors.length > 0) { + frappe.throw(errors.join("
    ")); + } +} + +function is_call_schedule_overlapped(day_schedule) { + // Check if any time slots are overlapped in a day schedule. + let timeslots = []; + day_schedule.forEach((record)=> { + timeslots.push([time_to_seconds(record.from_time), time_to_seconds(record.to_time)]); + }); + + if (timeslots.length < 2) { + return false; + } + + timeslots = number_sort(timeslots); + + // Sorted timeslots will be in ascending order if not overlapped. + for (let i=1; i < timeslots.length; i++) { + if (check_timeslot_overlap(timeslots[i-1], timeslots[i])) { + return true; + } + } + return false; +} + +function validate_call_schedule_overlaps(schedule) { + let group_by_day = groupby(schedule, 'day_of_week'); + for (const [day, day_schedule] of Object.entries(group_by_day)) { + if (is_call_schedule_overlapped(day_schedule)) { + frappe.throw(__('Please fix overlapping time slots for {0}', [day])); + } + } +} + +frappe.ui.form.on('Incoming Call Settings', { + validate(frm) { + validate_call_schedule(frm.doc.call_handling_schedule); + } +}); + diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json new file mode 100644 index 00000000000..3ffb3e49db1 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-11-19 10:37:20.734245", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "call_routing", + "column_break_2", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message", + "section_break_6", + "call_handling_schedule" + ], + "fields": [ + { + "default": "Sequential", + "fieldname": "call_routing", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Call Routing", + "options": "Sequential\nSimultaneous" + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "call_handling_schedule", + "fieldtype": "Table", + "label": "Call Handling Schedule", + "options": "Incoming Call Handling Schedule", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-19 11:17:14.527862", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py new file mode 100644 index 00000000000..2b2008a8ab7 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -0,0 +1,63 @@ +# -*- 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 datetime import datetime +from typing import Tuple +from frappe import _ + +class IncomingCallSettings(Document): + def validate(self): + """List of validations + * Make sure that to time slot is ahead of from time slot in call schedule + * Make sure that no overlapping timeslots for a given day + """ + self.validate_call_schedule_timeslot(self.call_handling_schedule) + self.validate_call_schedule_overlaps(self.call_handling_schedule) + + def validate_call_schedule_timeslot(self, schedule: list): + """ Make sure that to time slot is ahead of from time slot. + """ + errors = [] + for record in schedule: + from_time = self.time_to_seconds(record.from_time) + to_time = self.time_to_seconds(record.to_time) + if from_time >= to_time: + errors.append( + _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + ) + + if errors: + frappe.throw('
    '.join(errors)) + + def validate_call_schedule_overlaps(self, schedule: list): + """Check if any time slots are overlapped in a day schedule. + """ + week_days = set([each.day_of_week for each in schedule]) + + for day in week_days: + timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + + # convert time in timeslot into an integer represents number of seconds + timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) + if len(timeslots) < 2: continue + + for i in range(1, len(timeslots)): + if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): + frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + + @staticmethod + def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: + if (ts1[0] < ts2[0] and ts1[1] <= ts2[0]) or (ts1[0] >= ts2[1] and ts1[1] > ts2[1]): + return False + return True + + @staticmethod + def time_to_seconds(time: str) -> int: + """Convert time string of format HH:MM:SS into seconds + """ + date_time = datetime.strptime(time, "%H:%M:%S") + return date_time - datetime(1900, 1, 1) diff --git a/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py new file mode 100644 index 00000000000..c058c117b32 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomingCallSettings(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/voice_call_settings/__init__.py b/erpnext/telephony/doctype/voice_call_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py new file mode 100644 index 00000000000..85d6adda093 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVoiceCallSettings(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js new file mode 100644 index 00000000000..4a61b612d00 --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Voice Call Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json new file mode 100644 index 00000000000..25e55a22dce --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2020-12-08 16:52:40.590146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "call_receiving_device", + "column_break_3", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "permlevel": 1, + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "default": "Computer", + "fieldname": "call_receiving_device", + "fieldtype": "Select", + "label": "Call Receiving Device", + "options": "Computer\nPhone" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-14 18:49:34.600194", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Voice Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "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 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py new file mode 100644 index 00000000000..ad3bbf1784d --- /dev/null +++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py @@ -0,0 +1,10 @@ +# -*- 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 + +class VoiceCallSettings(Document): + pass diff --git a/erpnext/templates/print_formats/includes/taxes.html b/erpnext/templates/print_formats/includes/taxes.html index 6e984f39016..304e845287c 100644 --- a/erpnext/templates/print_formats/includes/taxes.html +++ b/erpnext/templates/print_formats/includes/taxes.html @@ -20,10 +20,10 @@ {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
    -
    + +
    - {{ frappe.format_value(frappe.utils.flt(charge.tax_amount), - table_meta.get_field("tax_amount"), doc, currency=doc.currency) }} + {{ charge.get_formatted('tax_amount', doc) }}
    {%- endif -%}