diff --git a/CODEOWNERS b/CODEOWNERS index e406f8f56e..b4503017f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,7 +4,7 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 331adb4b8e..b4df0a5270 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -40,7 +40,7 @@ class Dunning(AccountsController): def on_cancel(self): if self.dunning_amount: - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def make_gl_entries(self): diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 154fdc039d..675a3287fa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -234,7 +234,7 @@ class PaymentReconciliation(Document): def allocate_entries(self, args): self.validate_entries() - invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices")) + invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments")) default_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) @@ -253,6 +253,9 @@ class PaymentReconciliation(Document): pay["amount"] = 0 inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) + if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]: + pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name")) + res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") @@ -407,13 +410,21 @@ class PaymentReconciliation(Document): if not self.get("payments"): frappe.throw(_("No records found in the Payments table")) - def get_invoice_exchange_map(self, invoices): + def get_invoice_exchange_map(self, invoices, payments): sales_invoices = [ d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" ] + + sales_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"] + ) purchase_invoices = [ d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" ] + purchase_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"] + ) + invoice_exchange_map = frappe._dict() if sales_invoices: diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 00e3934f10..f9dda0593b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -473,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase): invoices = [x.as_dict() for x in pr.get("invoices")] payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() pr.get_unreconciled_entries() @@ -506,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase): payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocation[0].allocated_amount = allocated_amount + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() # assert outstanding diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index fc837c75a3..52eb29b3bb 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -45,21 +45,20 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): - existing_payment_request_amount = get_existing_payment_request_amount( - self.reference_doctype, self.reference_name + existing_payment_request_amount = flt( + get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) - if existing_payment_request_amount: - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": - ref_amount = get_amount(ref_doc, self.payment_account) + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": + ref_amount = get_amount(ref_doc, self.payment_account) - if existing_payment_request_amount + flt(self.grand_total) > ref_amount: - frappe.throw( - _("Total Payment Request amount cannot be greater than {0} amount").format( - self.reference_doctype - ) + if existing_payment_request_amount + flt(self.grand_total) > ref_amount: + frappe.throw( + _("Total Payment Request amount cannot be greater than {0} amount").format( + self.reference_doctype ) + ) def validate_currency(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a03de9e194..2608c03ffe 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1512,9 +1512,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ref_doc = frappe.get_doc(voucher_type, voucher_no) # Didn't use db_set for optimisation purpose - ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] + ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0 frappe.db.set_value( - voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] + voucher_type, + voucher_no, + "outstanding_amount", + outstanding["outstanding_in_account_currency"] or 0.0, ) ref_doc.set_status(update=True) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 9fcb769bc8..fc6793a9bb 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -252,6 +252,7 @@ def get_already_returned_items(doc): child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code + for update """.format( column, doc.doctype, doc.doctype ), diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json index 723c6d993d..c3cedcc7a6 100644 --- a/erpnext/crm/doctype/lead_source/lead_source.json +++ b/erpnext/crm/doctype/lead_source/lead_source.json @@ -26,10 +26,11 @@ } ], "links": [], - "modified": "2021-02-08 12:51:48.971517", + "modified": "2023-02-10 00:51:44.973957", "modified_by": "Administrator", "module": "CRM", "name": "Lead Source", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -58,5 +59,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 77aa559b77..caf8ff5b36 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -18,10 +18,11 @@ } ], "links": [], - "modified": "2020-05-20 12:22:01.866472", + "modified": "2023-02-10 01:40:23.713390", "modified_by": "Administrator", "module": "CRM", "name": "Sales Stage", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -40,5 +41,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1 + "states": [], + "track_changes": 1, + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js index a227b6d797..458c79a1ea 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -11,6 +11,40 @@ frappe.query_reports["Loan Interest Report"] = { "options": "Company", "default": frappe.defaults.get_user_default("Company"), "reqd": 1 - } + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + }, + { + "fieldname":"to_date", + "label": __("From Date"), + "fieldtype": "Date", + }, ] }; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 9186ce6174..58a7880a45 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -13,12 +13,12 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic def execute(filters=None): - columns = get_columns(filters) + columns = get_columns() data = get_active_loan_details(filters) return columns, data -def get_columns(filters): +def get_columns(): columns = [ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, @@ -70,6 +70,13 @@ def get_columns(filters): "options": "currency", "width": 120, }, + { + "label": _("Accrued Principal"), + "fieldname": "accrued_principal", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, { "label": _("Total Repayment"), "fieldname": "total_repayment", @@ -137,11 +144,16 @@ def get_columns(filters): def get_active_loan_details(filters): - - filter_obj = {"status": ("!=", "Closed")} + filter_obj = { + "status": ("!=", "Closed"), + "docstatus": 1, + } if filters.get("company"): filter_obj.update({"company": filters.get("company")}) + if filters.get("applicant"): + filter_obj.update({"applicant": filters.get("applicant")}) + loan_details = frappe.get_all( "Loan", fields=[ @@ -167,8 +179,8 @@ def get_active_loan_details(filters): sanctioned_amount_map = get_sanctioned_amount_map() penal_interest_rate_map = get_penal_interest_rate_map() - payments = get_payments(loan_list) - accrual_map = get_interest_accruals(loan_list) + payments = get_payments(loan_list, filters) + accrual_map = get_interest_accruals(loan_list, filters) currency = erpnext.get_company_currency(filters.get("company")) for loan in loan_details: @@ -183,6 +195,7 @@ def get_active_loan_details(filters): - flt(loan.written_off_amount), "total_repayment": flt(payments.get(loan.loan)), "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "accrued_principal": flt(accrual_map.get(loan.loan, {}).get("accrued_principal")), "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty_interest": penal_interest_rate_map.get(loan.loan_type), @@ -212,20 +225,35 @@ def get_sanctioned_amount_map(): ) -def get_payments(loans): +def get_payments(loans, filters): + query_filters = {"against_loan": ("in", loans)} + + if filters.get("from_date"): + query_filters.update({"posting_date": (">=", filters.get("from_date"))}) + + if filters.get("to_date"): + query_filters.update({"posting_date": ("<=", filters.get("to_date"))}) + return frappe._dict( frappe.get_all( "Loan Repayment", fields=["against_loan", "sum(amount_paid)"], - filters={"against_loan": ("in", loans)}, + filters=query_filters, group_by="against_loan", as_list=1, ) ) -def get_interest_accruals(loans): +def get_interest_accruals(loans, filters): accrual_map = {} + query_filters = {"loan": ("in", loans)} + + if filters.get("from_date"): + query_filters.update({"posting_date": (">=", filters.get("from_date"))}) + + if filters.get("to_date"): + query_filters.update({"posting_date": ("<=", filters.get("to_date"))}) interest_accruals = frappe.get_all( "Loan Interest Accrual", @@ -236,8 +264,9 @@ def get_interest_accruals(loans): "penalty_amount", "paid_interest_amount", "accrual_type", + "payable_principal_amount", ], - filters={"loan": ("in", loans)}, + filters=query_filters, order_by="posting_date desc", ) @@ -246,6 +275,7 @@ def get_interest_accruals(loans): entry.loan, { "accrued_interest": 0.0, + "accrued_principal": 0.0, "undue_interest": 0.0, "interest_outstanding": 0.0, "last_accrual_date": "", @@ -270,6 +300,7 @@ def get_interest_accruals(loans): accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount + accrual_map[entry.loan]["accrued_principal"] += entry.payable_principal_amount if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: accrual_map[entry.loan]["penalty"] = entry.penalty_amount diff --git a/erpnext/loan_management/workspace/loans/loans.json b/erpnext/loan_management/workspace/loans/loans.json new file mode 100644 index 0000000000..c65be4efae --- /dev/null +++ b/erpnext/loan_management/workspace/loans/loans.json @@ -0,0 +1,315 @@ +{ + "charts": [], + "content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "creation": "2020-03-12 16:35:55.299820", + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "loan", + "idx": 0, + "is_hidden": 0, + "label": "Loans", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Type", + "link_count": 0, + "link_to": "Loan Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Application", + "link_count": 0, + "link_to": "Loan Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan", + "link_count": 0, + "link_to": "Loan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan Processes", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Process Loan Security Shortfall", + "link_count": 0, + "link_to": "Process Loan Security Shortfall", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Process Loan Interest Accrual", + "link_count": 0, + "link_to": "Process Loan Interest Accrual", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Disbursement and Repayment", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Disbursement", + "link_count": 0, + "link_to": "Loan Disbursement", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Repayment", + "link_count": 0, + "link_to": "Loan Repayment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Write Off", + "link_count": 0, + "link_to": "Loan Write Off", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Interest Accrual", + "link_count": 0, + "link_to": "Loan Interest Accrual", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Type", + "link_count": 0, + "link_to": "Loan Security Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Price", + "link_count": 0, + "link_to": "Loan Security Price", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security", + "link_count": 0, + "link_to": "Loan Security", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Pledge", + "link_count": 0, + "link_to": "Loan Security Pledge", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Unpledge", + "link_count": 0, + "link_to": "Loan Security Unpledge", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Security Shortfall", + "link_count": 0, + "link_to": "Loan Security Shortfall", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 6, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 1, + "label": "Loan Repayment and Closure", + "link_count": 0, + "link_to": "Loan Repayment and Closure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Status", + "link_count": 0, + "link_to": "Loan Security Status", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Interest Report", + "link_count": 0, + "link_to": "Loan Interest Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Exposure", + "link_count": 0, + "link_to": "Loan Security Exposure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Applicant-Wise Loan Security Exposure", + "link_count": 0, + "link_to": "Applicant-Wise Loan Security Exposure", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Loan Security Status", + "link_count": 0, + "link_to": "Loan Security Status", + "link_type": "Report", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2023-01-31 19:47:13.114415", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loans", + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "restrict_to_domain": "", + "roles": [], + "sequence_id": 16.0, + "shortcuts": [ + { + "color": "Green", + "format": "{} Open", + "label": "Loan Application", + "link_to": "Loan Application", + "stats_filter": "{ \"status\": \"Open\" }", + "type": "DocType" + }, + { + "label": "Loan", + "link_to": "Loan", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Dashboard", + "link_to": "Loan Dashboard", + "type": "Dashboard" + } + ], + "title": "Loans" +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index c2b331fcfd..db699b94d8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -289,7 +289,7 @@ { "fieldname": "scrap_items", "fieldtype": "Table", - "label": "Items", + "label": "Scrap Items", "options": "BOM Scrap Item" }, { @@ -605,7 +605,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-10 07:47:08.652616", + "modified": "2023-02-13 17:31:37.504565", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index 2d37fd69b1..72fd424b24 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -1,16 +1,17 @@ import frappe -from frappe import _ def execute(): - from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type + from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines frappe.reload_doc("selling", "doctype", "sales_partner_type") frappe.local.lang = frappe.db.get_default("lang") or "en" + default_sales_partner_type = read_lines("sales_partner_type.txt") + for s in default_sales_partner_type: - insert_sales_partner_type(_(s)) + insert_sales_partner_type(s) # get partner type in existing forms (customized) # and create a document if not created diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index e098c3e3c4..828a55e7bc 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -161,6 +161,37 @@ class TestTimesheet(unittest.TestCase): to_time = timesheet.time_logs[0].to_time self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True)) + def test_per_billed_hours(self): + """If amounts are 0, per_billed should be calculated based on hours.""" + ts = frappe.new_doc("Timesheet") + ts.total_billable_amount = 0 + ts.total_billed_amount = 0 + ts.total_billable_hours = 2 + + ts.total_billed_hours = 0.5 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 25) + + ts.total_billed_hours = 2 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 100) + + def test_per_billed_amount(self): + """If amounts are > 0, per_billed should be calculated based on amounts, regardless of hours.""" + ts = frappe.new_doc("Timesheet") + ts.total_billable_hours = 2 + ts.total_billed_hours = 1 + ts.total_billable_amount = 200 + ts.total_billed_amount = 50 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 25) + + ts.total_billed_hours = 3 + ts.total_billable_amount = 200 + ts.total_billed_amount = 200 + ts.calculate_percentage_billed() + self.assertEqual(ts.per_billed, 100) + def make_timesheet( employee, diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index f3bd09a67a..d482a46053 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -64,6 +64,8 @@ class Timesheet(Document): self.per_billed = 0 if self.total_billed_amount > 0 and self.total_billable_amount > 0: self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount + elif self.total_billed_hours > 0 and self.total_billable_hours > 0: + self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours def update_billing_hours(self, args): if args.is_billable: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2ce0c7eb00..a87c3ec951 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -126,7 +126,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; - item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); + + if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) { + item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); + } + else { + let qty = item.qty || 1; + qty = me.frm.doc.is_return ? -1 * qty : qty; + item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); + } + item.item_tax_amount = 0.0; item.total_weight = flt(item.weight_per_unit * item.stock_qty); diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index 6c49f0f6dd..3c8ab8e47a 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -1,123 +1,68 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:industry", - "beta": 0, - "creation": "2012-03-27 14:36:09", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:industry", + "creation": "2012-03-27 14:36:09", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "industry" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "industry", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Industry", - "length": 0, - "no_copy": 0, - "oldfieldname": "industry", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "industry", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Industry", + "oldfieldname": "industry", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Industry Type", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [], + "modified": "2023-02-10 03:14:40.735763", + "modified_by": "Administrator", + "module": "Selling", + "name": "Industry Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6b42e4daea..b348bd3575 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -85,11 +85,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { - this.frm.add_custom_button( - __("Sales Order"), - this.frm.cscript["Make Sales Order"], - __("Create") - ); + if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation + || (!doc.valid_till) + || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { + 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/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 6836d56647..063813b2dc 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -195,6 +195,17 @@ def get_list_context(context=None): @frappe.whitelist() def make_sales_order(source_name: str, target_doc=None): + if not frappe.db.get_singles_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): + frappe.throw(_("Validity period of this quotation has ended.")) + return _make_sales_order(source_name, target_doc) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 5aaba4fa43..cdf5f5d00c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -144,11 +144,21 @@ class TestQuotation(FrappeTestCase): def test_so_from_expired_quotation(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0 + ) + quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(nowdate(), -1) quotation.insert() quotation.submit() + self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1 + ) + make_sales_order(quotation.name) def test_shopping_cart_without_website_item(self): diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index ccea8407ab..9559f134c1 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -17,6 +17,8 @@ "customer_name", "tax_id", "order_type", + "col_breaktest123", + "test_my_field", "column_break_7", "transaction_date", "delivery_date", @@ -248,6 +250,15 @@ "print_hide": 1, "reqd": 1 }, + { + "fieldname": "col_breaktest123", + "fieldtype": "Column Break" + }, + { + "fieldname": "test_my_field", + "fieldtype": "Data", + "label": "Test My Field" + }, { "fieldname": "column_break1", "fieldtype": "Column Break", @@ -1643,7 +1654,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:34:00.681780", + "modified": "2023-02-13 11:59:00.681780", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1722,4 +1733,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json index e7dd0d84a0..a9b500a625 100644 --- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json +++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json @@ -1,94 +1,47 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:sales_partner_type", - "beta": 0, - "creation": "2018-06-11 13:15:57.404716", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "field:sales_partner_type", + "creation": "2018-06-11 13:15:57.404716", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_partner_type", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Sales Partner Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "sales_partner_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Partner Type", + "reqd": 1, + "unique": 1 } - ], - "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-06-11 13:45:13.554307", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Partner Type", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2023-02-10 01:00:20.110800", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Partner Type", + "naming_rule": "By fieldname", + "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, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 73bfbd32cc..50c30e6fc4 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -27,6 +27,7 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", + "allow_sales_order_creation_for_expired_quotation", "dont_reserve_sales_order_qty_on_sales_return", "hide_tax_id", "enable_discount_accounting" @@ -174,6 +175,12 @@ "fieldtype": "Check", "label": "Enable Discount Accounting for Selling" }, + { + "default": "0", + "fieldname": "allow_sales_order_creation_for_expired_quotation", + "fieldtype": "Check", + "label": "Allow Sales Order Creation For Expired Quotation" + }, { "default": "0", "fieldname": "dont_reserve_sales_order_qty_on_sales_return", @@ -186,7 +193,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-06-17 15:50:43.968334", + "modified": "2023-02-04 12:37:53.380857", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -215,4 +222,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 0a356b9a6f..89ce61ab16 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -322,6 +322,11 @@ erpnext.PointOfSale.Payment = class { this.focus_on_default_mop(); } + after_render() { + const frm = this.events.get_frm(); + frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname); + } + edit_cart() { this.events.toggle_other_sections(false); this.toggle_component(false); @@ -332,6 +337,7 @@ erpnext.PointOfSale.Payment = class { this.toggle_component(true); this.render_payment_section(); + this.after_render(); } toggle_remarks_control() { diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 991ac719cd..990d736baa 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -103,6 +103,11 @@ function get_filters() { return options } }, + { + "fieldname":"only_immediate_upcoming_term", + "label": __("Show only the Immediate Upcoming Term"), + "fieldtype": "Check", + }, ] return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 8bf56865a7..3682c5fd62 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -4,6 +4,7 @@ import frappe from frappe import _, qb, query_builder from frappe.query_builder import Criterion, functions +from frappe.utils.dateutils import getdate def get_columns(): @@ -208,6 +209,7 @@ def get_so_with_invoices(filters): ) .where( (so.docstatus == 1) + & (so.status.isin(["To Deliver and Bill", "To Bill"])) & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) @@ -291,6 +293,18 @@ def filter_on_calculated_status(filters, sales_orders): return sales_orders +def filter_for_immediate_upcoming_term(filters, sales_orders): + if filters.only_immediate_upcoming_term and sales_orders: + immediate_term_found = set() + filtered_data = [] + for order in sales_orders: + if order.name not in immediate_term_found and order.due_date > getdate(): + filtered_data.append(order) + immediate_term_found.add(order.name) + return filtered_data + return sales_orders + + def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) @@ -298,6 +312,8 @@ def execute(filters=None): sales_orders = filter_on_calculated_status(filters, sales_orders) + sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders) + prepare_chart(sales_orders) data = sales_orders diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 63d339a839..29691230f2 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -175,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters): # update existing entry so_row = sales_order_map[so_name] so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) - so_row["delay"] = min(so_row["delay"], row["delay"]) + so_row["delay"] = ( + min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"] + ) # sum numeric columns fields = [ diff --git a/erpnext/setup/doctype/designation/designation.json b/erpnext/setup/doctype/designation/designation.json index 2cbbb04ed9..a5b2ac9128 100644 --- a/erpnext/setup/doctype/designation/designation.json +++ b/erpnext/setup/doctype/designation/designation.json @@ -31,7 +31,7 @@ "icon": "fa fa-bookmark", "idx": 1, "links": [], - "modified": "2022-06-28 17:10:26.853753", + "modified": "2023-02-10 01:53:41.319386", "modified_by": "Administrator", "module": "Setup", "name": "Designation", @@ -58,5 +58,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", - "states": [] + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js index 3680906057..c3605bf0e8 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js @@ -1,13 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - -//--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { - -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +// frappe.ui.form.on("Terms and Conditions", { +// refresh(frm) {} +// }); diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index f14b243512..f884864acf 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -33,7 +33,6 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "in_list_view": 1, "label": "Disabled" }, { @@ -60,12 +59,14 @@ "default": "1", "fieldname": "selling", "fieldtype": "Check", + "in_list_view": 1, "label": "Selling" }, { "default": "1", "fieldname": "buying", "fieldtype": "Check", + "in_list_view": 1, "label": "Buying" }, { @@ -76,10 +77,11 @@ "icon": "icon-legal", "idx": 1, "links": [], - "modified": "2022-06-16 15:07:38.094844", + "modified": "2023-02-01 14:33:39.246532", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -133,5 +135,6 @@ "quick_entry": 1, "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/data/designation.txt b/erpnext/setup/setup_wizard/data/designation.txt new file mode 100644 index 0000000000..4c6d7bdea8 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/designation.txt @@ -0,0 +1,31 @@ +Accountant +Administrative Assistant +Administrative Officer +Analyst +Associate +Business Analyst +Business Development Manager +Consultant +Chief Executive Officer +Chief Financial Officer +Chief Operating Officer +Chief Technology Officer +Customer Service Representative +Designer +Engineer +Executive Assistant +Finance Manager +HR Manager +Head of Marketing and Sales +Manager +Managing Director +Marketing Manager +Marketing Specialist +President +Product Manager +Project Manager +Researcher +Sales Representative +Secretary +Software Developer +Vice President diff --git a/erpnext/setup/setup_wizard/data/industry_type.py b/erpnext/setup/setup_wizard/data/industry_type.py deleted file mode 100644 index 0bc3f32eb0..0000000000 --- a/erpnext/setup/setup_wizard/data/industry_type.py +++ /dev/null @@ -1,57 +0,0 @@ -from frappe import _ - - -def get_industry_types(): - return [ - _("Accounting"), - _("Advertising"), - _("Aerospace"), - _("Agriculture"), - _("Airline"), - _("Apparel & Accessories"), - _("Automotive"), - _("Banking"), - _("Biotechnology"), - _("Broadcasting"), - _("Brokerage"), - _("Chemical"), - _("Computer"), - _("Consulting"), - _("Consumer Products"), - _("Cosmetics"), - _("Defense"), - _("Department Stores"), - _("Education"), - _("Electronics"), - _("Energy"), - _("Entertainment & Leisure"), - _("Executive Search"), - _("Financial Services"), - _("Food, Beverage & Tobacco"), - _("Grocery"), - _("Health Care"), - _("Internet Publishing"), - _("Investment Banking"), - _("Legal"), - _("Manufacturing"), - _("Motion Picture & Video"), - _("Music"), - _("Newspaper Publishers"), - _("Online Auctions"), - _("Pension Funds"), - _("Pharmaceuticals"), - _("Private Equity"), - _("Publishing"), - _("Real Estate"), - _("Retail & Wholesale"), - _("Securities & Commodity Exchanges"), - _("Service"), - _("Soap & Detergent"), - _("Software"), - _("Sports"), - _("Technology"), - _("Telecommunications"), - _("Television"), - _("Transportation"), - _("Venture Capital"), - ] diff --git a/erpnext/setup/setup_wizard/data/industry_type.txt b/erpnext/setup/setup_wizard/data/industry_type.txt new file mode 100644 index 0000000000..eadc689e31 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/industry_type.txt @@ -0,0 +1,51 @@ +Accounting +Advertising +Aerospace +Agriculture +Airline +Apparel & Accessories +Automotive +Banking +Biotechnology +Broadcasting +Brokerage +Chemical +Computer +Consulting +Consumer Products +Cosmetics +Defense +Department Stores +Education +Electronics +Energy +Entertainment & Leisure +Executive Search +Financial Services +Food, Beverage & Tobacco +Grocery +Health Care +Internet Publishing +Investment Banking +Legal +Manufacturing +Motion Picture & Video +Music +Newspaper Publishers +Online Auctions +Pension Funds +Pharmaceuticals +Private Equity +Publishing +Real Estate +Retail & Wholesale +Securities & Commodity Exchanges +Service +Soap & Detergent +Software +Sports +Technology +Telecommunications +Television +Transportation +Venture Capital diff --git a/erpnext/setup/setup_wizard/data/lead_source.txt b/erpnext/setup/setup_wizard/data/lead_source.txt new file mode 100644 index 0000000000..00ca1808bb --- /dev/null +++ b/erpnext/setup/setup_wizard/data/lead_source.txt @@ -0,0 +1,10 @@ +Existing Customer +Reference +Advertisement +Cold Calling +Exhibition +Supplier Reference +Mass Mailing +Customer's Vendor +Campaign +Walk In diff --git a/erpnext/setup/setup_wizard/data/sales_partner_type.txt b/erpnext/setup/setup_wizard/data/sales_partner_type.txt new file mode 100644 index 0000000000..68e9b9ac73 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_partner_type.txt @@ -0,0 +1,7 @@ +Channel Partner +Distributor +Dealer +Agent +Retailer +Implementation Partner +Reseller diff --git a/erpnext/setup/setup_wizard/data/sales_stage.txt b/erpnext/setup/setup_wizard/data/sales_stage.txt new file mode 100644 index 0000000000..2808ce7985 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_stage.txt @@ -0,0 +1,8 @@ +Prospecting +Qualification +Needs Analysis +Value Proposition +Identifying Decision Makers +Perception Analysis +Proposal/Price Quote +Negotiation/Review diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4d9b871e5e..6bc17718ae 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -4,6 +4,7 @@ import json import os +from pathlib import Path import frappe from frappe import _ @@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = [ - "Existing Customer", - "Reference", - "Advertisement", - "Cold Calling", - "Exhibition", - "Supplier Reference", - "Mass Mailing", - "Customer's Vendor", - "Campaign", - "Walk In", -] -default_sales_partner_type = [ - "Channel Partner", - "Distributor", - "Dealer", - "Agent", - "Retailer", - "Implementation Partner", - "Reseller", -] +def read_lines(filename: str) -> list[str]: + """Return a list of lines from a file in the data directory.""" + return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines() def install(country=None): @@ -85,7 +68,11 @@ def install(country=None): # Stock Entry Type {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, - {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer", + "purpose": "Material Transfer", + }, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, { @@ -103,22 +90,6 @@ def install(country=None): "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", }, - # Designation - {"doctype": "Designation", "designation_name": _("CEO")}, - {"doctype": "Designation", "designation_name": _("Manager")}, - {"doctype": "Designation", "designation_name": _("Analyst")}, - {"doctype": "Designation", "designation_name": _("Engineer")}, - {"doctype": "Designation", "designation_name": _("Accountant")}, - {"doctype": "Designation", "designation_name": _("Secretary")}, - {"doctype": "Designation", "designation_name": _("Associate")}, - {"doctype": "Designation", "designation_name": _("Administrative Officer")}, - {"doctype": "Designation", "designation_name": _("Business Development Manager")}, - {"doctype": "Designation", "designation_name": _("HR Manager")}, - {"doctype": "Designation", "designation_name": _("Project Manager")}, - {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, - {"doctype": "Designation", "designation_name": _("Software Developer")}, - {"doctype": "Designation", "designation_name": _("Designer")}, - {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World { "doctype": "Territory", @@ -291,28 +262,18 @@ def install(country=None): {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages - {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, - {"doctype": "Sales Stage", "stage_name": _("Qualification")}, - {"doctype": "Sales Stage", "stage_name": _("Needs Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Value Proposition")}, - {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, - {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, # Warehouse Type {"doctype": "Warehouse Type", "name": "Transit"}, ] - from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - - records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] - - records += [ - {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type - ] + for doctype, title_field, filename in ( + ("Designation", "designation_name", "designation.txt"), + ("Sales Stage", "stage_name", "sales_stage.txt"), + ("Industry Type", "industry", "industry_type.txt"), + ("Lead Source", "source_name", "lead_source.txt"), + ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"), + ): + records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file( @@ -335,16 +296,11 @@ def install(country=None): make_default_records() make_records(records) set_up_address_templates(default_country=country) - set_more_defaults() - update_global_search_doctypes() - - -def set_more_defaults(): - # Do more setup stuff that can be done here with no dependencies update_selling_defaults() update_buying_defaults() add_uom_data() update_item_variant_settings() + update_global_search_doctypes() def update_selling_defaults(): @@ -381,7 +337,7 @@ def add_uom_data(): ) for d in uoms: if not frappe.db.exists("UOM", _(d.get("uom_name"))): - uom_doc = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM", "uom_name": _(d.get("uom_name")), @@ -402,9 +358,10 @@ def add_uom_data(): frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() if not frappe.db.exists( - "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + "UOM Conversion Factor", + {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}, ): - uom_conversion = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM Conversion Factor", "category": _(d.get("category")), @@ -412,7 +369,7 @@ def add_uom_data(): "to_uom": _(d.get("to_uom")), "value": d.get("value"), } - ).insert(ignore_permissions=True) + ).db_insert() def add_market_segments(): @@ -468,7 +425,7 @@ def install_company(args): make_records(records) -def install_defaults(args=None): +def install_defaults(args=None): # nosemgrep records = [ # Price Lists { @@ -493,7 +450,7 @@ def install_defaults(args=None): # enable default currency frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) - frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) + frappe.db.set_single_value("Stock Settings", "email_footer_address", args.get("company_name")) set_global_defaults(args) update_stock_settings() @@ -540,7 +497,8 @@ def create_bank_account(args): company_name = args.get("company_name") bank_account_group = frappe.db.get_value( - "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + "Account", + {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}, ) if bank_account_group: bank_account = frappe.get_doc( diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 2f77dd6ae5..49ba78c63a 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -158,6 +158,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True + doc.flags.ignore_mandatory = True doc.insert(ignore_permissions=True) return doc diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index bb120eaa6b..62936fcfb8 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -25,6 +25,12 @@ def boot_session(bootinfo): frappe.db.get_single_value("CRM Settings", "default_valid_till") ) + bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint( + frappe.db.get_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ) + ) + # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] diff --git a/erpnext/stock/doctype/item_price/test_records.json b/erpnext/stock/doctype/item_price/test_records.json index 0a3d7e8198..afe5ad65b7 100644 --- a/erpnext/stock/doctype/item_price/test_records.json +++ b/erpnext/stock/doctype/item_price/test_records.json @@ -38,5 +38,19 @@ "price_list_rate": 1000, "valid_from": "2017-04-10", "valid_upto": "2017-04-17" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Buying Price List", + "price_list_rate": 100, + "supplier": "_Test Supplier" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Selling Price List", + "price_list_rate": 200, + "customer": "_Test Customer" } ] diff --git a/erpnext/stock/doctype/price_list/test_records.json b/erpnext/stock/doctype/price_list/test_records.json index 7ca949c402..e02a7adbd8 100644 --- a/erpnext/stock/doctype/price_list/test_records.json +++ b/erpnext/stock/doctype/price_list/test_records.json @@ -31,5 +31,21 @@ "enabled": 1, "price_list_name": "_Test Price List Rest of the World", "selling": 1 + }, + { + "buying": 0, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Selling Price List", + "selling": 1 + }, + { + "buying": 1, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Buying Price List", + "selling": 0 } ] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8c20ca0211..7f69397fce 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2494,7 +2494,7 @@ def get_uom_details(item_code, uom, qty): if not conversion_factor: frappe.msgprint( - _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) ) ret = {"uom": ""} else: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5af144110f..b53f429edf 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -88,8 +88,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) + # Never try to find a customer price if customer is set in these Doctype + current_customer = args.customer + if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + args.customer = None + out.update(get_price_list_rate(args, item)) + args.customer = current_customer + if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index a8e056c992..ab6e18e967 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -103,7 +103,7 @@ def get_reserved_qty(item_code, warehouse): q.inner_join(SalesOrder) .on(SalesOrder.name == child_table.parent) .where(SalesOrder.docstatus == 1) - .where(SalesOrder.status != "Closed") + .where(SalesOrder.status.notin(["On Hold", "Closed"])) ) SalesOrder = DocType("Sales Order") diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py new file mode 100644 index 0000000000..b53e29e9e8 --- /dev/null +++ b/erpnext/stock/tests/test_get_item_details.py @@ -0,0 +1,40 @@ +import json + +import frappe +from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase + +from erpnext.stock.get_item_details import get_item_details + +test_ignore = ["BOM"] +test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"] + + +class TestGetItemDetail(FrappeTestCase): + def setUp(self): + make_test_records("Price List") + super().setUp() + + def test_get_item_detail_purchase_order(self): + + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "customer": "_Test Customer", + "conversion_rate": 1.0, + "price_list_currency": "USD", + "plc_conversion_rate": 1.0, + "doctype": "Purchase Order", + "name": None, + "supplier": "_Test Supplier", + "transaction_date": None, + "conversion_rate": 1.0, + "price_list": "_Test Buying Price List", + "is_subcontracted": 0, + "ignore_pricing_rule": 1, + "qty": 1, + } + ) + details = get_item_details(args) + self.assertEqual(details.get("price_list_rate"), 100) diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 8a92418d25..219747c9f8 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -51,21 +51,31 @@ def get_tabs(categories): return tab_values -def get_category_records(categories): +def get_category_records(categories: list): categorical_data = {} - for category in categories: - if category == "item_group": + + for c in categories: + if c == "item_group": categorical_data["item_group"] = frappe.db.get_all( "Item Group", filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, fields=["name", "parent_item_group", "is_group", "image", "route"], ) - else: - doctype = frappe.unscrub(category) - fields = ["name"] - if frappe.get_meta(doctype, cached=True).get_field("image"): + + continue + + doctype = frappe.unscrub(c) + fields = ["name"] + + try: + meta = frappe.get_meta(doctype, cached=True) + if meta.get_field("image"): fields += ["image"] - categorical_data[category] = frappe.db.get_all(doctype, fields=fields) + data = frappe.db.get_all(doctype, fields=fields) + categorical_data[c] = data + except BaseException: + frappe.throw(_("DocType {} not found").format(doctype)) + continue return categorical_data