diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index 63cc46518ff..7e57c2fc471 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -4,6 +4,23 @@ frappe.ui.form.on("Bank Clearance", { setup: function(frm) { frm.add_fetch("account", "account_currency", "account_currency"); + + frm.set_query("account", function() { + return { + "filters": { + "account_type": ["in",["Bank","Cash"]], + "is_group": 0, + } + }; + }); + + frm.set_query("bank_account", function () { + return { + filters: { + 'is_company_account': 1 + }, + }; + }); }, onload: function(frm) { @@ -12,14 +29,7 @@ frappe.ui.form.on("Bank Clearance", { locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; frm.set_value("account", default_bank_account); - frm.set_query("account", function() { - return { - "filters": { - "account_type": ["in",["Bank","Cash"]], - "is_group": 0 - } - }; - }); + frm.set_value("from_date", frappe.datetime.month_start()); frm.set_value("to_date", frappe.datetime.month_end()); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7f245fd083a..94874894b0f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -995,7 +995,9 @@ class PaymentEntry(AccountsController): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): - frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() + frappe.get_doc( + d.reference_doctype, d.reference_name, for_update=True + ).set_total_advance_paid() def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 0a39a62ef12..5c1cb0dcc68 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -215,6 +215,7 @@ "reqd": 1 }, { + "default": "1", "depends_on": "eval:doc.uom != doc.stock_uom", "fieldname": "conversion_factor", "fieldtype": "Float", @@ -821,6 +822,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -878,7 +880,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:37:29.032732", + "modified": "2022-10-26 16:05:37.304788", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index afd5a59df4e..0c03c550ba0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2017,6 +2017,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): update_address( target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address ) + update_address( + target_doc, "billing_address", "billing_address_display", source_doc.customer_address + ) if currency: target_doc.currency = currency diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index a307a6c17cd..7f1a1eccc45 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -247,6 +247,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -876,7 +877,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-10 20:57:38.340026", + "modified": "2022-10-26 11:38:36.119339", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index a95e0a9c2da..f3acdc5aa87 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -3,6 +3,7 @@ import frappe +from dateutil import relativedelta from frappe import _ from frappe.model.document import Document from frappe.utils import date_diff, flt, get_first_day, get_last_day, getdate @@ -49,7 +50,7 @@ def get_plan_rate( start_date = getdate(start_date) end_date = getdate(end_date) - no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1 + no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1 cost = plan.cost * no_of_months # Adjust cost if start or end date is not month start or end diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 7cf14e67385..e1a30a4b77e 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -51,6 +51,8 @@ frappe.query_reports["Accounts Payable"] = { } else { frappe.query_report.set_filter_value('tax_id', ""); } + + frappe.query_report.refresh(); } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 8557c03bd5e..f2ee1eb10ea 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -748,7 +748,7 @@ class ReceivablePayableReport(object): self.add_accounting_dimensions_filters() - def get_cost_center_conditions(self, conditions): + def get_cost_center_conditions(self): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) cost_center_list = [ center.name diff --git a/erpnext/accounts/report/payment_ledger/__init__.py b/erpnext/accounts/report/payment_ledger/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js new file mode 100644 index 00000000000..9779844dc94 --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -0,0 +1,59 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"account", + "label": __("Account"), + "fieldtype": "MultiSelectList", + "options": "Account", + get_data: function(txt) { + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company") + }); + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + { + "fieldname":"against_voucher_no", + "label": __("Against Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + + ] + return filters; +} + +frappe.query_reports["Payment Ledger"] = { + "filters": get_filters() +}; diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.json b/erpnext/accounts/report/payment_ledger/payment_ledger.json new file mode 100644 index 00000000000..716329fbeff --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2022-06-06 08:50:43.933708", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2022-06-06 08:50:43.933708", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Ledger", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Payment Ledger Entry", + "report_name": "Payment Ledger", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.py b/erpnext/accounts/report/payment_ledger/payment_ledger.py new file mode 100644 index 00000000000..e470c2727e3 --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.py @@ -0,0 +1,222 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict + +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion + + +class PaymentLedger(object): + def __init__(self, filters=None): + self.filters = filters + self.columns, self.data = [], [] + self.voucher_dict = OrderedDict() + self.voucher_amount = [] + self.ple = qb.DocType("Payment Ledger Entry") + + def init_voucher_dict(self): + + if self.voucher_amount: + s = set() + # build a set of unique vouchers + for ple in self.voucher_amount: + key = (ple.voucher_type, ple.voucher_no, ple.party) + s.add(key) + + # for each unique vouchers, initialize +/- list + for key in s: + self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list()) + + # for each ple, using against voucher and amount, assign it to +/- list + # group by against voucher + for ple in self.voucher_amount: + against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + target = None + if self.voucher_dict.get(against_key): + if ple.amount > 0: + target = self.voucher_dict.get(against_key).increase + else: + target = self.voucher_dict.get(against_key).decrease + + # this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple) + # need to somehow include the stray entries as well. + if target is not None: + entry = frappe._dict( + company=ple.company, + account=ple.account, + party_type=ple.party_type, + party=ple.party, + voucher_type=ple.voucher_type, + voucher_no=ple.voucher_no, + against_voucher_type=ple.against_voucher_type, + against_voucher_no=ple.against_voucher_no, + amount=ple.amount, + currency=ple.account_currency, + ) + + if self.filters.include_account_currency: + entry["amount_in_account_currency"] = ple.amount_in_account_currency + + target.append(entry) + + def build_data(self): + self.data.clear() + + for value in self.voucher_dict.values(): + voucher_data = [] + if value.increase != []: + voucher_data.extend(value.increase) + if value.decrease != []: + voucher_data.extend(value.decrease) + + if voucher_data: + # balance row + total = 0 + total_in_account_currency = 0 + + for x in voucher_data: + total += x.amount + if self.filters.include_account_currency: + total_in_account_currency += x.amount_in_account_currency + + entry = frappe._dict( + against_voucher_no="Outstanding:", + amount=total, + currency=voucher_data[0].currency, + ) + + if self.filters.include_account_currency: + entry["amount_in_account_currency"] = total_in_account_currency + + voucher_data.append(entry) + + # empty row + voucher_data.append(frappe._dict()) + self.data.extend(voucher_data) + + def build_conditions(self): + self.conditions = [] + + if self.filters.company: + self.conditions.append(self.ple.company == self.filters.company) + + if self.filters.account: + self.conditions.append(self.ple.account.isin(self.filters.account)) + + if self.filters.period_start_date: + self.conditions.append(self.ple.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + self.conditions.append(self.ple.posting_date.lte(self.filters.period_end_date)) + + if self.filters.voucher_no: + self.conditions.append(self.ple.voucher_no == self.filters.voucher_no) + + if self.filters.against_voucher_no: + self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no) + + def get_data(self): + ple = self.ple + + self.build_conditions() + + # fetch data from table + self.voucher_amount = ( + qb.from_(ple) + .select(ple.star) + .where(ple.delinked == 0) + .where(Criterion.all(self.conditions)) + .run(as_dict=True) + ) + + def get_columns(self): + options = None + self.columns.append( + dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100") + ) + + self.columns.append( + dict(label=_("Account"), fieldname="account", fieldtype="data", options=options, width="100") + ) + + self.columns.append( + dict( + label=_("Party Type"), fieldname="party_type", fieldtype="data", options=options, width="100" + ) + ) + self.columns.append( + dict(label=_("Party"), fieldname="party", fieldtype="data", options=options, width="100") + ) + self.columns.append( + dict( + label=_("Voucher Type"), + fieldname="voucher_type", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100" + ) + ) + self.columns.append( + dict( + label=_("Against Voucher Type"), + fieldname="against_voucher_type", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Against Voucher No"), + fieldname="against_voucher_no", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Amount"), + fieldname="amount", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + if self.filters.include_account_currency: + self.columns.append( + dict( + label=_("Amount in Account Currency"), + fieldname="amount_in_account_currency", + fieldtype="Currency", + options="currency", + width="100", + ) + ) + self.columns.append( + dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True) + ) + + def run(self): + self.get_columns() + self.get_data() + + # initialize dictionary and group using against voucher + self.init_voucher_dict() + + # convert dictionary to list and add balance rows + self.build_data() + + return self.columns, self.data + + +def execute(filters=None): + return PaymentLedger(filters).run() diff --git a/erpnext/accounts/report/payment_ledger/test_payment_ledger.py b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py new file mode 100644 index 00000000000..5ae9b87cde9 --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py @@ -0,0 +1,65 @@ +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.payment_ledger.payment_ledger import execute + + +class TestPaymentLedger(FrappeTestCase): + def setUp(self): + self.create_company() + self.cleanup() + + def cleanup(self): + doctypes = [] + doctypes.append(qb.DocType("GL Entry")) + doctypes.append(qb.DocType("Payment Ledger Entry")) + doctypes.append(qb.DocType("Sales Invoice")) + doctypes.append(qb.DocType("Payment Entry")) + + for doctype in doctypes: + qb.from_(doctype).delete().where(doctype.company == self.company).run() + + def create_company(self): + name = "Test Payment Ledger" + company = None + if frappe.db.exists("Company", name): + company = frappe.get_doc("Company", name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "All Warehouses" + " - " + company.abbr + self.income_account = company.default_income_account + self.expense_account = company.default_expense_account + self.debit_to = company.default_receivable_account + + def test_unpaid_invoice_outstanding(self): + sinv = create_sales_invoice( + company=self.company, + debit_to=self.debit_to, + expense_account=self.expense_account, + cost_center=self.cost_center, + income_account=self.income_account, + warehouse=self.warehouse, + ) + pe = get_payment_entry(sinv.doctype, sinv.name).save().submit() + + filters = frappe._dict({"company": self.company}) + columns, data = execute(filters=filters) + outstanding = [x for x in data if x.get("against_voucher_no") == "Outstanding:"] + self.assertEqual(outstanding[0].get("amount"), 0) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 82e92e87bc9..b8203bd1286 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -777,6 +777,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -894,7 +895,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-07 11:12:38.634976", + "modified": "2022-10-26 16:47:41.364387", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 55722686fe4..e2dbf21be2c 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from erpnext.accounts.party import get_due_date @@ -152,6 +153,40 @@ class TestSupplier(FrappeTestCase): # Rollback address.delete() + def test_serach_fields_for_supplier(self): + from erpnext.controllers.queries import supplier_query + + supplier_name = create_supplier(supplier_name="Test Supplier 1").name + + make_property_setter( + "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype" + ) + + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertTrue("supplier_type" not in data[0]) + + make_property_setter( + "Supplier", + None, + "search_fields", + "supplier_group, supplier_type", + "Data", + for_doctype="Doctype", + ) + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertEqual(data[0].supplier_type, "Company") + self.assertTrue("supplier_type" in data[0]) + def create_supplier(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8eae0a07028..3bdc0170682 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -78,18 +78,16 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def customer_query(doctype, txt, searchfield, start, page_len, filters): +def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Customer" conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") - if cust_master_name == "Customer Name": - fields = ["name", "customer_group", "territory"] - else: - fields = ["name", "customer_name", "customer_group", "territory"] + fields = ["name"] + if cust_master_name != "Customer Name": + fields = ["customer_name"] fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) @@ -112,20 +110,20 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) # searches for supplier @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def supplier_query(doctype, txt, searchfield, start, page_len, filters): +def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] + fields = ["name"] + if supp_master_name != "Supplier Name": + fields = ["supplier_name"] fields = get_fields(doctype, fields) @@ -145,6 +143,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 38328e69674..20e2b0b2013 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', { }, refresh: function (frm) { + if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") { + frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date"); + } + if (frm.doc.docstatus == 1) { if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { frm.add_custom_button(__('Request Loan Closure'), function() { @@ -103,6 +107,14 @@ frappe.ui.form.on('Loan', { frm.trigger("toggle_fields"); }, + repayment_schedule_type: function(frm) { + if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") { + frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date"); + } else { + frm.set_df_property("repayment_start_date", "label", "Repayment Start Date"); + } + }, + loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); frm.toggle_display("repayment_method", frm.doc.is_term_loan); diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 47488f43ce3..dc8b03e89d3 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -18,6 +18,7 @@ "status", "section_break_8", "loan_type", + "repayment_schedule_type", "loan_amount", "rate_of_interest", "is_secured_loan", @@ -158,7 +159,8 @@ "depends_on": "is_term_loan", "fieldname": "repayment_start_date", "fieldtype": "Date", - "label": "Repayment Start Date" + "label": "Repayment Start Date", + "mandatory_depends_on": "is_term_loan" }, { "fieldname": "column_break_11", @@ -402,12 +404,20 @@ "fieldname": "is_npa", "fieldtype": "Check", "label": "Is NPA" + }, + { + "depends_on": "is_term_loan", + "fetch_from": "loan_type.repayment_schedule_type", + "fieldname": "repayment_schedule_type", + "fieldtype": "Data", + "label": "Repayment Schedule Type", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-07-12 11:50:31.957360", + "modified": "2022-09-30 10:36:47.902903", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index d84eef6d8ca..0c9c97f60fd 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -7,7 +7,16 @@ import math import frappe from frappe import _ -from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + date_diff, + flt, + get_last_day, + getdate, + now_datetime, + nowdate, +) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry @@ -107,30 +116,81 @@ class Loan(AccountsController): if not self.repayment_start_date: frappe.throw(_("Repayment Start Date is mandatory for term loans")) + schedule_type_details = frappe.db.get_value( + "Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1 + ) + self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount - while balance_amount > 0: - interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100)) - principal_amount = self.monthly_repayment_amount - interest_amount - balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) - if balance_amount < 0: - principal_amount += balance_amount - balance_amount = 0.0 - total_payment = principal_amount + interest_amount - self.append( - "repayment_schedule", - { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount, - }, + while balance_amount > 0: + interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts( + payment_date, + balance_amount, + schedule_type_details.repayment_schedule_type, + schedule_type_details.repayment_date_on, ) - next_payment_date = add_single_month(payment_date) - payment_date = next_payment_date + + if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months": + next_payment_date = get_last_day(payment_date) + if schedule_type_details.repayment_date_on == "Start of the next month": + next_payment_date = add_days(next_payment_date, 1) + + payment_date = next_payment_date + + self.add_repayment_schedule_row( + payment_date, principal_amount, interest_amount, total_payment, balance_amount + ) + + if ( + schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date" + or schedule_type_details.repayment_date_on == "End of the current month" + ): + next_payment_date = add_single_month(payment_date) + payment_date = next_payment_date + + def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on): + if schedule_type == "Monthly as per repayment start date": + days = 1 + months = 12 + else: + expected_payment_date = get_last_day(payment_date) + if repayment_date_on == "Start of the next month": + expected_payment_date = add_days(expected_payment_date, 1) + + if expected_payment_date == payment_date: + # using 30 days for calculating interest for all full months + days = 30 + months = 365 + else: + days = date_diff(get_last_day(payment_date), payment_date) + months = 365 + + interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) + principal_amount = self.monthly_repayment_amount - interest_amount + balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) + if balance_amount < 0: + principal_amount += balance_amount + balance_amount = 0.0 + + total_payment = principal_amount + interest_amount + + return interest_amount, principal_amount, balance_amount, total_payment + + def add_repayment_schedule_row( + self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount + ): + self.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_loan_amount, + }, + ) def set_repayment_period(self): if self.repayment_method == "Repay Fixed Amount per Period": diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index da05c8e90c4..388e65d9e58 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -4,7 +4,16 @@ import unittest import frappe -from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + add_to_date, + date_diff, + flt, + format_date, + get_datetime, + nowdate, +) from erpnext.loan_management.doctype.loan.loan import ( make_loan_write_off, @@ -47,6 +56,51 @@ class TestLoan(unittest.TestCase): loan_account="Loan Account - _TC", interest_income_account="Interest Income Account - _TC", penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", + ) + + create_loan_type( + "Term Loan Type 1", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", + ) + + create_loan_type( + "Term Loan Type 2", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Pro-rated calendar months", + repayment_date_on="Start of the next month", + ) + + create_loan_type( + "Term Loan Type 3", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Pro-rated calendar months", + repayment_date_on="End of the current month", ) create_loan_type( @@ -62,6 +116,7 @@ class TestLoan(unittest.TestCase): "Loan Account - _TC", "Interest Income Account - _TC", "Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", ) create_loan_type( @@ -902,6 +957,69 @@ class TestLoan(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0) + def test_term_loan_schedule_types(self): + loan = create_loan( + self.applicant1, + "Term Loan Type 1", + 12000, + "Repay Over Number of Periods", + 12, + repayment_start_date="2022-10-17", + ) + + # Check for first, second and last installment date + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023" + ) + + loan.loan_type = "Term Loan Type 2" + loan.save() + + # Check for first, second and last installment date + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023" + ) + + loan.loan_type = "Term Loan Type 3" + loan.save() + + # Check for first, second and last installment date + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023" + ) + + loan.repayment_method = "Repay Fixed Amount per Period" + loan.monthly_repayment_amount = 1042 + loan.save() + + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023" + ) + def create_loan_scenario_for_penalty(doc): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] @@ -1033,6 +1151,8 @@ def create_loan_type( penalty_income_account=None, repayment_method=None, repayment_periods=None, + repayment_schedule_type=None, + repayment_date_on=None, ): if not frappe.db.exists("Loan Type", loan_name): @@ -1042,6 +1162,7 @@ def create_loan_type( "company": "_Test Company", "loan_name": loan_name, "is_term_loan": is_term_loan, + "repayment_schedule_type": "Monthly as per repayment start date", "maximum_loan_amount": maximum_loan_amount, "rate_of_interest": rate_of_interest, "penalty_interest_rate": penalty_interest_rate, @@ -1056,8 +1177,14 @@ def create_loan_type( "repayment_periods": repayment_periods, "write_off_amount": 100, } - ).insert() + ) + if loan_type.is_term_loan: + loan_type.repayment_schedule_type = repayment_schedule_type + if loan_type.repayment_schedule_type != "Monthly as per repayment start date": + loan_type.repayment_date_on = repayment_date_on + + loan_type.insert() loan_type.submit() diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 00337e4b4c3..5cc9464585b 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -16,6 +16,8 @@ "company", "is_term_loan", "disabled", + "repayment_schedule_type", + "repayment_date_on", "description", "account_details_section", "mode_of_payment", @@ -157,12 +159,30 @@ "label": "Disbursement Account", "options": "Account", "reqd": 1 + }, + { + "depends_on": "is_term_loan", + "description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)", + "fieldname": "repayment_schedule_type", + "fieldtype": "Select", + "label": "Repayment Schedule Type", + "mandatory_depends_on": "is_term_loan", + "options": "\nMonthly as per repayment start date\nPro-rated calendar months" + }, + { + "depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"", + "description": "Select whether the repayment date should be the end of the current month or start of the upcoming month", + "fieldname": "repayment_date_on", + "fieldtype": "Select", + "label": "Repayment Date On", + "mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"", + "options": "\nStart of the next month\nEnd of the current month" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-25 16:23:57.009349", + "modified": "2022-10-22 17:43:03.954201", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ff84991c36e..580838e215b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -385,6 +385,7 @@ class BOM(WebsiteGenerator): if self.docstatus == 2: return + self.flags.cost_updated = False existing_bom_cost = self.total_cost if self.docstatus == 1: @@ -407,7 +408,11 @@ class BOM(WebsiteGenerator): frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) if not from_child_bom: - frappe.msgprint(_("Cost Updated"), alert=True) + msg = "Cost Updated" + if not self.flags.cost_updated: + msg = "No changes in cost found" + + frappe.msgprint(_(msg), alert=True) def update_parent_cost(self): if self.total_cost: @@ -593,11 +598,16 @@ class BOM(WebsiteGenerator): # not via doc event, table is not regenerated and needs updation self.calculate_exploded_cost() + old_cost = self.total_cost + self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = ( self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost ) + if self.total_cost != old_cost: + self.flags.cost_updated = True + def calculate_op_cost(self, update_hour_rate=False): """Update workstation rate and calculates totals""" self.operating_cost = 0 diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 27f3cc905b9..e34ac12cd23 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -9,7 +9,10 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt -from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on +from erpnext.controllers.tests.test_subcontracting_controller import ( + make_stock_in_entry, + set_backflush_based_on, +) from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( update_cost_in_all_boms_in_test, @@ -639,6 +642,28 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + def test_bom_cost_update_flag(self): + rm_item = make_item( + properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} + ).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item]) + + create_stock_reconciliation( + item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600 + ) + + bom.load_from_db() + bom.update_cost() + self.assertTrue(bom.flags.cost_updated) + + bom.load_from_db() + bom.update_cost() + self.assertFalse(bom.flags.cost_updated) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ed45106634d..fb94e8aa994 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -133,7 +133,7 @@ class JobCard(Document): (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0} ) and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1} - order by jctl.to_time desc limit 1""".format( + order by jctl.to_time desc""".format( extra_cond, validate_overlap_for ), { diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ac7114138c3..4d2dab73e3a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -136,6 +136,45 @@ class TestJobCard(FrappeTestCase): ) self.assertRaises(OverlapError, jc2.save) + def test_job_card_overlap_with_capacity(self): + wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + + workstation = make_workstation(workstation_name=random_string(5)).name + frappe.db.set_value("Workstation", workstation, "production_capacity", 1) + + jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name}) + + jc1.workstation = workstation + jc1.append( + "time_logs", + {"from_time": "2021-01-01 00:00:00", "to_time": "2021-01-01 08:00:00", "completed_qty": 1}, + ) + jc1.save() + + jc2.workstation = workstation + + # add a new entry in same time slice + jc2.append( + "time_logs", + {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1}, + ) + self.assertRaises(OverlapError, jc2.save) + + frappe.db.set_value("Workstation", workstation, "production_capacity", 2) + jc2.load_from_db() + + jc2.workstation = workstation + + # add a new entry in same time slice + jc2.append( + "time_logs", + {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1}, + ) + + jc2.save() + self.assertTrue(jc2.name) + def test_job_card_multiple_materials_transfer(self): "Test transferring RMs separately against Job Card with multiple RMs." self.transfer_material_against = "Job Card" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 4bb4dcc648e..000ee07f2c1 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -27,6 +27,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock.get_item_details import get_conversion_factor from erpnext.utilities.transaction_base import validate_uom_is_integer @@ -648,13 +649,23 @@ class ProductionPlan(Document): else: material_request = material_request_map[key] + conversion_factor = 1.0 + if ( + material_request_type == "Purchase" + and item_doc.purchase_uom + and item_doc.purchase_uom != item_doc.stock_uom + ): + conversion_factor = ( + get_conversion_factor(item_doc.name, item_doc.purchase_uom).get("conversion_factor") or 1.0 + ) + # add item material_request.append( "items", { "item_code": item.item_code, "from_warehouse": item.from_warehouse, - "qty": item.quantity, + "qty": item.quantity / conversion_factor, "schedule_date": schedule_date, "warehouse": item.warehouse, "sales_order": item.sales_order, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 60e63980724..c4ab0f886fa 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -806,6 +806,35 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(pln.status, "Completed") self.assertEqual(pln.po_items[0].produced_qty, 5) + def test_material_request_item_for_purchase_uom(self): + from erpnext.stock.doctype.item.test_item import make_item + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 10}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1" + ) + + pln.make_material_request() + for row in frappe.get_all( + "Material Request Item", + filters={"production_plan": pln.name}, + fields=["item_code", "uom", "qty"], + ): + self.assertEqual(row.item_code, bom_item) + self.assertEqual(row.uom, "Nos") + self.assertEqual(row.qty, 1) + def create_production_plan(**args): """ diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fb937f6cd18..1605c521d3e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,5 +315,6 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v14_0.update_tds_fields \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_schedule_type_in_loans.py b/erpnext/patches/v13_0/update_schedule_type_in_loans.py new file mode 100644 index 00000000000..e5b5f643604 --- /dev/null +++ b/erpnext/patches/v13_0/update_schedule_type_in_loans.py @@ -0,0 +1,14 @@ +import frappe + + +def execute(): + loan = frappe.qb.DocType("Loan") + loan_type = frappe.qb.DocType("Loan Type") + + frappe.qb.update(loan_type).set( + loan_type.repayment_schedule_type, "Monthly as per repayment start date" + ).where(loan_type.is_term_loan == 1).run() + + frappe.qb.update(loan).set( + loan.repayment_schedule_type, "Monthly as per repayment start date" + ).where(loan.is_term_loan == 1).run() diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 1a309ba0156..b0082bdb281 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -28,7 +28,7 @@ erpnext.financial_statements = { }, "open_general_ledger": function(data) { if (!data.account) return; - var project = $.grep(frappe.query_report.filters, function(e){ return e.df.fieldname == 'project'; }) + let project = $.grep(frappe.query_report.filters, function(e){ return e.df.fieldname == 'project'; }); frappe.route_options = { "account": data.account, @@ -37,7 +37,16 @@ erpnext.financial_statements = { "to_date": data.to_date || data.year_end_date, "project": (project && project.length > 0) ? project[0].$input.val() : "" }; - frappe.set_route("query-report", "General Ledger"); + + let report = "General Ledger"; + + if (["Payable", "Receivable"].includes(data.account_type)) { + report = data.account_type == "Payable" ? "Accounts Payable" : "Accounts Receivable"; + frappe.route_options["party_account"] = data.account; + frappe.route_options["report_date"] = data.year_end_date; + } + + frappe.set_route("query-report", report); }, "tree": true, "name_field": "account", diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 58594b0a13d..644adff1e27 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { }); }; -erpnext.utils.get_contact_details = function(frm) { +erpnext.utils.get_contact_details = function (frm) { if (frm.updating_party_details) return; if (frm.doc["contact_person"]) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", - args: {contact: frm.doc.contact_person }, - callback: function(r) { - if (r.message) - frm.set_value(r.message); - } - }) + args: { contact: frm.doc.contact_person }, + callback: function (r) { + if (r.message) frm.set_value(r.message); + }, + }); + } else { + frm.set_value({ + contact_person: "", + contact_display: "", + contact_email: "", + contact_mobile: "", + contact_phone: "", + contact_designation: "", + contact_department: "", + }); } -} +}; erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { if (!value) { diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7dc3fab6239..691adccd4dd 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase from frappe.utils import flt @@ -341,6 +342,33 @@ class TestCustomer(FrappeTestCase): due_date = get_due_date("2017-01-22", "Customer", "_Test Customer") self.assertEqual(due_date, "2017-01-22") + def test_serach_fields_for_customer(self): + from erpnext.controllers.queries import customer_query + + make_property_setter( + "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype" + ) + + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertTrue("territory" not in data[0]) + + make_property_setter( + "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype" + ) + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertEqual(data[0].territory, "_Test Territory") + self.assertTrue("territory" in data[0]) + def get_customer_dict(customer_name): return { diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 70ae085051d..6b42e4daead 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -84,11 +84,12 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } } - if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) { - if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { - cur_frm.add_custom_button(__('Sales Order'), - cur_frm.cscript['Make Sales Order'], __('Create')); - } + if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { + this.frm.add_custom_button( + __("Sales Order"), + this.frm.cscript["Make Sales Order"], + __("Create") + ); if(doc.status!=="Ordered") { this.frm.add_custom_button(__('Set as Lost'), () => { diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 2cf836f9fcc..ea0b25f41cc 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -272,6 +272,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -842,7 +843,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-09-06 13:24:18.065312", + "modified": "2022-10-26 16:05:02.712705", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 36d5a6ce0e1..9dd28dc60b3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -842,6 +842,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): update_address( target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address ) + update_address( + target_doc, "billing_address", "billing_address_display", source_doc.customer_address + ) update_taxes( target_doc, 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 0a5cbabab0b..77c32534321 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -261,6 +261,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -814,7 +815,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:36:05.344847", + "modified": "2022-10-26 16:05:17.720768", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index 06ba9365561..b3ae7b58b49 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -12,13 +12,17 @@ def get_data(): "Purchase Receipt": "return_against", }, "internal_links": { + "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], "Project": ["items", "project"], "Quality Inspection": ["items", "quality_inspection"], }, "transactions": [ {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, - {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]}, + { + "label": _("Reference"), + "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"], + }, {"label": _("Returns"), "items": ["Purchase Receipt"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, ], 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 772736e0adc..474ee92e268 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -919,6 +919,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -1000,7 +1001,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:37:59.516609", + "modified": "2022-10-26 16:06:02.524435", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9fb3be5188e..b8c5187b2c3 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -13,6 +13,8 @@ from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext from erpnext.stock.valuation import FIFOValuation, LIFOValuation +BarcodeScanResult = Dict[str, Optional[str]] + class InvalidWarehouseCompany(frappe.ValidationError): pass @@ -552,7 +554,16 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool @frappe.whitelist() -def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: +def scan_barcode(search_value: str) -> BarcodeScanResult: + def set_cache(data: BarcodeScanResult): + frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) + + def get_cache() -> Optional[BarcodeScanResult]: + if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): + return data + + if scan_data := get_cache(): + return scan_data # search barcode no barcode_data = frappe.db.get_value( @@ -562,7 +573,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if barcode_data: - return _update_item_info(barcode_data) + _update_item_info(barcode_data) + set_cache(barcode_data) + return barcode_data # search serial no serial_no_data = frappe.db.get_value( @@ -572,7 +585,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if serial_no_data: - return _update_item_info(serial_no_data) + _update_item_info(serial_no_data) + set_cache(serial_no_data) + return serial_no_data # search batch no batch_no_data = frappe.db.get_value( @@ -582,7 +597,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if batch_no_data: - return _update_item_info(batch_no_data) + _update_item_info(batch_no_data) + set_cache(batch_no_data) + return batch_no_data return {}