diff --git a/.github/helper/.flake8_strict b/.github/helper/.flake8_strict index 198ec7bfe54..3e8f7dd11ab 100644 --- a/.github/helper/.flake8_strict +++ b/.github/helper/.flake8_strict @@ -66,7 +66,8 @@ ignore = F841, E713, E712, - B023 + B023, + B028 max-line-length = 200 diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b96..c62b711f2c2 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "api_details_section", + "disabled", "service_provider", "api_endpoint", "url", @@ -77,12 +78,18 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-10 15:51:14.521174", + "modified": "2023-01-09 12:19:03.955906", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 3f69d5c7cd8..498fc7c295f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -137,8 +137,7 @@ "fieldname": "finance_book", "fieldtype": "Link", "label": "Finance Book", - "options": "Finance Book", - "read_only": 1 + "options": "Finance Book" }, { "fieldname": "2_add_edit_gl_entries", @@ -539,7 +538,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-11-28 17:40:01.241908", + "modified": "2023-01-17 12:53:53.280620", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ac033f7db60..12c0b7a7bf7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -334,7 +334,7 @@ class PaymentReconciliation(Document): ) # Account Currency has balance - dr_or_cr = "debit" if self.party_type == "Customer" else "debit" + dr_or_cr = "debit" if self.party_type == "Customer" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" journal_account = frappe._dict( @@ -471,6 +471,7 @@ class PaymentReconciliation(Document): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): self.common_filter_conditions.clear() + self.accounting_dimension_filter_conditions.clear() self.ple_posting_date_filter.clear() ple = qb.DocType("Payment Ledger Entry") diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 0da44a464e7..3920d4cf096 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -49,7 +49,6 @@
{% endif %} - {{ _("Against") }}: {{ row.against }}
{{ _("Remarks") }}: {{ row.remarks }} {% if row.bill_no %}
{{ _("Supplier Invoice No") }}: {{ row.bill_no }} diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py index 9f6828fb733..209cad4f905 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py @@ -7,7 +7,6 @@ import frappe from frappe import _, qb from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.utils.background_jobs import is_job_queued from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry @@ -27,7 +26,7 @@ def start_payment_ledger_repost(docname=None): """ if docname: repost_doc = frappe.get_doc("Repost Payment Ledger", docname) - if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]: + if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]: try: for entry in repost_doc.repost_vouchers: doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) @@ -102,10 +101,9 @@ def execute_repost_payment_ledger(docname): job_name = "payment_ledger_repost_" + docname - if not is_job_queued(job_name): - frappe.enqueue( - method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost", - docname=docname, - is_async=True, - job_name=job_name, - ) + frappe.enqueue( + method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost", + docname=docname, + is_async=True, + job_name=job_name, + ) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b834d1404d0..1bce43fd310 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -259,9 +259,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if tax_deducted: net_total = inv.tax_withholding_net_total if ldc: - tax_amount = get_tds_amount_from_ldc( - ldc, parties, pan_no, tax_details, posting_date, net_total - ) + tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 @@ -538,7 +536,7 @@ def get_invoice_total_without_tcs(inv, tax_details): return inv.grand_total - tcs_tax_row_amount -def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): +def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): tds_amount = 0 limit_consumed = frappe.db.get_value( "Purchase Invoice", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index c04f518d7e6..475be92add5 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -25,8 +25,8 @@ {%= __("Date") %} - {%= __("Ref") %} - {%= __("Party") %} + {%= __("Reference") %} + {%= __("Remarks") %} {%= __("Debit") %} {%= __("Credit") %} {%= __("Balance (Dr - Cr)") %} @@ -45,7 +45,6 @@
{% } %} - {{ __("Against") }}: {%= data[i].against %}
{%= __("Remarks") %}: {%= data[i].remarks %} {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 98838907be1..bfe2a0fd2be 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.utils import flt def execute(filters=None): @@ -65,6 +66,12 @@ def get_result( else: total_amount_credited += entry.credit + ## Check if ldc is applied and show rate as per ldc + actual_rate = (tds_deducted / total_amount_credited) * 100 + + if flt(actual_rate) < flt(rate): + rate = actual_rate + if tds_deducted: row = { "pan" diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 445dcc53c63..a03de9e1940 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -439,8 +439,7 @@ def reconcile_against_document(args): # nosemgrep # cancel advance entry doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True - gl_map = doc.build_gl_map() - create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1) + _delete_pl_entries(voucher_type, voucher_no) for entry in entries: check_if_advance_entry_modified(entry) @@ -452,11 +451,23 @@ def reconcile_against_document(args): # nosemgrep else: update_reference_in_payment_entry(entry, doc, do_not_save=True) + if doc.doctype == "Journal Entry": + try: + doc.validate_total_debit_and_credit() + except Exception as validation_exception: + raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception + doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) gl_map = doc.build_gl_map() - create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1) + create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) + + # Only update outstanding for newly linked vouchers + for entry in entries: + update_voucher_outstanding( + entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party + ) frappe.flags.ignore_party_validation = False diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index b8185c929e6..8f5b85d1b2c 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -135,6 +135,10 @@ frappe.ui.form.on('Asset', { }, __("Manage")); } + if (frm.doc.depr_entry_posting_status === "Failed") { + frm.trigger("set_depr_posting_failure_alert"); + } + frm.trigger("setup_chart"); } @@ -145,6 +149,19 @@ frappe.ui.form.on('Asset', { } }, + set_depr_posting_failure_alert: function (frm) { + const alert = ` +
+
+ + Failed to post depreciation entries + +
+
`; + + frm.dashboard.set_headline_alert(alert); + }, + toggle_reference_doc: function(frm) { if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { frm.set_df_property('purchase_invoice', 'read_only', 1); diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 4bac3031e8a..8a64a953172 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -68,6 +68,7 @@ "column_break_51", "purchase_receipt_amount", "default_finance_book", + "depr_entry_posting_status", "amended_from" ], "fields": [ @@ -473,6 +474,16 @@ "fieldtype": "Int", "label": "Asset Quantity", "read_only_depends_on": "eval:!doc.is_existing_asset" + }, + { + "fieldname": "depr_entry_posting_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Depreciation Entry Posting Status", + "no_copy": 1, + "options": "\nSuccessful\nFailed", + "print_hide": 1, + "read_only": 1 } ], "idx": 72, @@ -487,7 +498,7 @@ { "group": "Repair", "link_doctype": "Asset Repair", - "link_fieldname": "asset_name" + "link_fieldname": "asset" }, { "group": "Value", @@ -500,7 +511,7 @@ "link_fieldname": "asset" } ], - "modified": "2022-11-25 12:47:19.689702", + "modified": "2023-01-17 00:25:30.387242", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 7686c348a63..5337fd64eea 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today +from frappe.utils.user import get_users_with_role from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, @@ -18,7 +19,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched ) -def post_depreciation_entries(date=None, commit=True): +def post_depreciation_entries(date=None): # Return if automatic booking of asset depreciation is disabled if not cint( frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically") @@ -27,13 +28,24 @@ def post_depreciation_entries(date=None, commit=True): if not date: date = today() + + failed_asset_names = [] + for asset_name in get_depreciable_assets(date): asset_doc = frappe.get_doc("Asset", asset_name) - make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) - - if commit: + try: + make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) frappe.db.commit() + except Exception as e: + frappe.db.rollback() + failed_asset_names.append(asset_name) + + if failed_asset_names: + set_depr_entry_posting_status_for_failed_assets(failed_asset_names) + notify_depr_entry_posting_error(failed_asset_names) + + frappe.db.commit() def get_depreciable_assets(date): @@ -146,6 +158,8 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): row.value_after_depreciation -= d.depreciation_amount row.db_update() + frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful") + asset.set_status() return asset_depr_schedule_doc @@ -209,6 +223,42 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation return credit_account, debit_account +def set_depr_entry_posting_status_for_failed_assets(failed_asset_names): + for asset_name in failed_asset_names: + frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed") + + +def notify_depr_entry_posting_error(failed_asset_names): + recipients = get_users_with_role("Accounts Manager") + + if not recipients: + recipients = get_users_with_role("System Manager") + + subject = _("Error while posting depreciation entries") + + asset_links = get_comma_separated_asset_links(failed_asset_names) + + message = ( + _("Hi,") + + "
" + + _("The following assets have failed to post depreciation entries: {0}").format(asset_links) + + "." + ) + + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + +def get_comma_separated_asset_links(asset_names): + asset_links = [] + + for asset_name in asset_names: + asset_links.append(get_link_to_form("Asset", asset_name)) + + asset_links = ", ".join(asset_links) + + return asset_links + + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) @@ -295,12 +345,12 @@ def reset_depreciation_schedule(asset_doc, date, notes): asset_doc, notes, date_of_return=date ) - modify_depreciation_schedule_for_asset_repairs(asset_doc) + modify_depreciation_schedule_for_asset_repairs(asset_doc, notes) asset_doc.save() -def modify_depreciation_schedule_for_asset_repairs(asset): +def modify_depreciation_schedule_for_asset_repairs(asset, notes): asset_repairs = frappe.get_all( "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] ) @@ -309,10 +359,6 @@ def modify_depreciation_schedule_for_asset_repairs(asset): if repair.increase_in_asset_life: asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair.modify_depreciation_schedule() - notes = _("This schedule was created when Asset {0} went through Asset Repair {1}.").format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(asset_repair.doctype, asset_repair.name), - ) make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d61ef8ecf8a..51a2b528979 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1549,6 +1549,7 @@ def create_asset(**args): "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, "asset_quantity": args.get("asset_quantity") or 1, + "depr_entry_posting_status": args.depr_entry_posting_status or "", } ) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 7d3b645be7d..821accf96ac 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -8,7 +8,6 @@ import frappe # import erpnext from frappe import _ from frappe.utils import cint, flt, get_link_to_form -from six import string_types import erpnext from erpnext.assets.doctype.asset.depreciation import ( @@ -431,7 +430,7 @@ class AssetCapitalization(StockController): if asset.calculate_depreciation: notes = _( - "This schedule was created when Asset {0} was consumed when Asset Capitalization {1} was submitted." + "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." ).format( get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name")) ) @@ -522,7 +521,7 @@ class AssetCapitalization(StockController): asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value notes = _( - "This schedule was created when target Asset {0} was updated when Asset Capitalization {1} was submitted." + "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}." ).format( get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) ) @@ -538,7 +537,7 @@ class AssetCapitalization(StockController): if asset.calculate_depreciation: reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) notes = _( - "This schedule was created when Asset {0} was restored when Asset Capitalization {1} was cancelled." + "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." ).format( get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) ) @@ -626,7 +625,7 @@ def get_target_asset_details(asset=None, company=None): @frappe.whitelist() def get_consumed_stock_item_details(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) @@ -678,7 +677,7 @@ def get_consumed_stock_item_details(args): @frappe.whitelist() def get_warehouse_details(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) @@ -694,7 +693,7 @@ def get_warehouse_details(args): @frappe.whitelist() def get_consumed_asset_details(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) @@ -746,7 +745,7 @@ def get_consumed_asset_details(args): @frappe.whitelist() def get_service_item_details(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index af09cda8fb3..898c4820791 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -159,7 +159,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-02 15:38:30.766779", + "modified": "2023-01-16 21:08:21.421260", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index b8cd115872c..9a05a74ef9d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -56,8 +56,11 @@ class AssetRepair(AccountsController): ): self.modify_depreciation_schedule() - notes = _("This schedule was created when Asset Repair {0} was submitted.").format( - get_link_to_form(self.doctype, self.name) + notes = _( + "This schedule was created when Asset {0} was repaired through Asset Repair {1}." + ).format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), ) self.asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) @@ -80,8 +83,9 @@ class AssetRepair(AccountsController): ): self.revert_depreciation_schedule_on_cancellation() - notes = _("This schedule was created when Asset Repair {0} was cancelled.").format( - get_link_to_form(self.doctype, self.name) + notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), ) self.asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 262d5529974..6cfbe53cf6a 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -127,12 +127,20 @@ class AssetValueAdjustment(Document): current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True current_asset_depr_schedule_doc.cancel() - notes = _( - "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.get("doctype"), self.get("name")), - ) + if self.docstatus == 1: + notes = _( + "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) + elif self.docstatus == 2: + notes = _( + "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) new_asset_depr_schedule_doc.notes = notes new_asset_depr_schedule_doc.insert() diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 572d9d3865c..f0360b27dc0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -889,6 +889,11 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(po.status, "Completed") self.assertEqual(mr.status, "Received") + def test_variant_item_po(self): + po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1) + + self.assertRaises(frappe.ValidationError, po.save) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -994,8 +999,8 @@ def create_purchase_order(**args): }, ) - po.set_missing_values() if not args.do_not_save: + po.set_missing_values() po.insert() if not args.do_not_submit: if po.is_subcontracted: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 788dc4982e5..6fa44c93c22 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -394,7 +394,7 @@ class AccountsController(TransactionBase): self.get("inter_company_reference") or self.get("inter_company_invoice_reference") or self.get("inter_company_order_reference") - ): + ) and not self.get("is_return"): msg = _("Internal Sale or Delivery Reference missing.") msg += _("Please create purchase from internal sale or delivery document itself") frappe.throw(msg, title=_("Internal Sales Reference Missing")) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 15c82af856b..8bd09982bf4 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -37,7 +37,7 @@ def validate_return_against(doc): if ( ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) - and ref_doc.docstatus == 1 + and ref_doc.docstatus.is_submitted() ): # validate posting date time return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index c6a634ba806..8c403aa9bfe 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _, scrub +from frappe.model.document import Document from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction import erpnext @@ -20,7 +21,7 @@ from erpnext.stock.get_item_details import _get_item_tax_template class calculate_taxes_and_totals(object): - def __init__(self, doc): + def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) @@ -677,7 +678,7 @@ class calculate_taxes_and_totals(object): ) def calculate_total_advance(self): - if self.doc.docstatus < 2: + if not self.doc.docstatus.is_cancelled(): total_allocated_amount = sum( flt(adv.allocated_amount, adv.precision("allocated_amount")) for adv in self.doc.get("advances") @@ -708,7 +709,7 @@ class calculate_taxes_and_totals(object): ) ) - if self.doc.docstatus == 0: + if self.doc.docstatus.is_draft(): if self.doc.get("write_off_outstanding_amount_automatically"): self.doc.write_off_amount = 0 diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index 832be2301c1..67bd24dd805 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = { reqd: 1 }, { - fieldname: "fiscal_year", - label: __("Fiscal Year"), - fieldtype: "Link", - options: "Fiscal Year", - default: frappe.defaults.get_user_default("fiscal_year"), - reqd: 1, - on_change: function(query_report) { - var fiscal_year = query_report.get_values().fiscal_year; - if (!fiscal_year) { - return; - } - frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { - var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); - frappe.query_report.set_filter_value({ - from_date: fy.year_start_date, - to_date: fy.year_end_date - }); - }); - } + label: __("Based On"), + fieldname:"based_on", + fieldtype: "Select", + options: "Creation Date\nPlanned Date\nActual Date", + default: "Creation Date" }, { label: __("From Posting Date"), fieldname:"from_date", fieldtype: "Date", - default: frappe.defaults.get_user_default("year_start_date"), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -3), reqd: 1 }, { label: __("To Posting Date"), fieldname:"to_date", fieldtype: "Date", - default: frappe.defaults.get_user_default("year_end_date"), + default: frappe.datetime.get_today(), reqd: 1, }, { diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index b69ad070e16..97f30ef62e9 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -31,6 +31,7 @@ def get_data(filters): "sales_order", "production_item", "qty", + "creation", "produced_qty", "planned_start_date", "planned_end_date", @@ -47,11 +48,17 @@ def get_data(filters): if filters.get(field): query_filters[field] = filters.get(field) - query_filters["planned_start_date"] = (">=", filters.get("from_date")) - query_filters["planned_end_date"] = ("<=", filters.get("to_date")) + if filters.get("based_on") == "Planned Date": + query_filters["planned_start_date"] = (">=", filters.get("from_date")) + query_filters["planned_end_date"] = ("<=", filters.get("to_date")) + elif filters.get("based_on") == "Actual Date": + query_filters["actual_start_date"] = (">=", filters.get("from_date")) + query_filters["actual_end_date"] = ("<=", filters.get("to_date")) + else: + query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")]) data = frappe.get_all( - "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1 ) res = [] @@ -213,6 +220,12 @@ def get_columns(filters): "options": "Sales Order", "width": 90, }, + { + "label": _("Created On"), + "fieldname": "creation", + "fieldtype": "Date", + "width": 150, + }, { "label": _("Planned Start Date"), "fieldname": "planned_start_date", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7495ab8d0bf..41067d8dfa8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -194,7 +194,6 @@ erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 erpnext.patches.v13_0.update_payment_terms_outstanding -erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings @@ -269,6 +268,7 @@ erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v15_0.delete_taxjar_doctypes erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets +erpnext.patches.v14_0.update_reference_due_date_in_journal_entry [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -291,6 +291,7 @@ erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs +erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v13_0.add_cost_center_in_loans diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index 75a5477be8f..c0d715063a8 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after def execute(): doctypes_to_reload = [ + ("setup", "company"), ("stock", "repost_item_valuation"), ("stock", "stock_entry_detail"), ("stock", "purchase_receipt_item"), diff --git a/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py b/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py new file mode 100644 index 00000000000..70003125a54 --- /dev/null +++ b/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + if frappe.db.get_value("Journal Entry Account", {"reference_due_date": ""}): + frappe.db.sql( + """ + UPDATE `tabJournal Entry Account` + SET reference_due_date = NULL + WHERE reference_due_date = '' + """ + ) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 2dde542d2d4..ce3ae4fc7ce 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -80,7 +80,7 @@ class Task(NestedSet): if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): frappe.throw( _( - "Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled." + "Cannot complete task {0} as its dependant task {1} are not completed / cancelled." ).format(frappe.bold(self.name), frappe.bold(d.task)) ) diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 51664f8885e..911343d8b64 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -1,7 +1,7 @@ frappe.provide("erpnext.accounts.bank_reconciliation"); erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { - constructor(company, bank_account) { + constructor(company, bank_account, bank_statement_from_date, bank_statement_to_date, filter_by_reference_date, from_reference_date, to_reference_date) { this.bank_account = bank_account; this.company = company; this.make_dialog(); diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 484b8c9f08d..6836d56647f 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -194,14 +194,7 @@ def get_list_context(context=None): @frappe.whitelist() -def make_sales_order(source_name, target_doc=None): - 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.")) +def make_sales_order(source_name: str, target_doc=None): 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 b151dd5e79c..5aaba4fa435 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -136,17 +136,20 @@ class TestQuotation(FrappeTestCase): sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30)) ) - def test_valid_till(self): - from erpnext.selling.doctype.quotation.quotation import make_sales_order - + def test_valid_till_before_transaction_date(self): quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(quotation.transaction_date, -1) self.assertRaises(frappe.ValidationError, quotation.validate) + def test_so_from_expired_quotation(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + + 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) + + make_sales_order(quotation.name) def test_shopping_cart_without_website_item(self): if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 7c0601e3dd5..accf5f22a6f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -208,7 +208,7 @@ class SalesOrder(SellingController): for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) - if doc.docstatus == 2: + if doc.docstatus.is_cancelled(): frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 5c4b57813d3..cbc40bbf90b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -14,7 +14,6 @@ def get_data(): }, "internal_links": { "Quotation": ["items", "prevdoc_docname"], - "Material Request": ["items", "material_request"], }, "transactions": [ { 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 134b5eafd08..d0dabad5c99 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -638,7 +638,6 @@ "width": "70px" }, { - "allow_on_submit": 1, "fieldname": "ordered_qty", "fieldtype": "Float", "label": "Ordered Qty", @@ -865,7 +864,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-12 13:13:28.691585", + "modified": "2022-12-25 02:51:10.247569", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 54bd8c355d6..bab57fe267a 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) + if frappe.get_cached_value( + "Currency Exchange Settings", "Currency Exchange Settings", "disabled" + ): + return 0.00 + try: cache = frappe.cache() key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 7e426ae4af8..53f6b7f8f17 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -106,7 +106,6 @@ class TestItem(FrappeTestCase): "conversion_factor": 1.0, "reserved_qty": 1, "actual_qty": 5, - "ordered_qty": 10, "projected_qty": 14, } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 9e6aead02db..808f19e2740 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -4,14 +4,14 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby -from typing import Dict, List, Set +from typing import Dict, List import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case -from frappe.query_builder.functions import Locate +from frappe.query_builder.functions import IfNull, Locate, Sum from frappe.utils import cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of @@ -41,7 +41,9 @@ class PickList(Document): ) def before_submit(self): - update_sales_orders = set() + self.validate_picked_items() + + def validate_picked_items(self): for item in self.locations: if self.scan_mode and item.picked_qty < item.stock_qty: frappe.throw( @@ -50,17 +52,14 @@ class PickList(Document): ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom), title=_("Pick List Incomplete"), ) - elif not self.scan_mode and item.picked_qty == 0: + + if not self.scan_mode and item.picked_qty == 0: # if the user has not entered any picked qty, set it to stock_qty, before submit item.picked_qty = item.stock_qty - if item.sales_order_item: - # update the picked_qty in SO Item - self.update_sales_order_item(item, item.picked_qty, item.item_code) - update_sales_orders.add(item.sales_order) - if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue + if not item.serial_no: frappe.throw( _("Row #{0}: {1} does not have any available serial numbers in {2}").format( @@ -68,58 +67,96 @@ class PickList(Document): ), title=_("Serial Nos Required"), ) - if len(item.serial_no.split("\n")) == item.picked_qty: - continue - frappe.throw( - _( - "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" - ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), - title=_("Quantity Mismatch"), - ) - self.update_bundle_picked_qty() - self.update_sales_order_picking_status(update_sales_orders) - - def before_cancel(self): - """Deduct picked qty on cancelling pick list""" - updated_sales_orders = set() - - for item in self.get("locations"): - if item.sales_order_item: - self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) - updated_sales_orders.add(item.sales_order) - - self.update_bundle_picked_qty() - self.update_sales_order_picking_status(updated_sales_orders) - - def update_sales_order_item(self, item, picked_qty, item_code): - item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" - stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" - - already_picked, actual_qty = frappe.db.get_value( - item_table, - item.sales_order_item, - ["picked_qty", stock_qty_field], - for_update=True, - ) - - if self.docstatus == 1: - if (((already_picked + picked_qty) / actual_qty) * 100) > ( - 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) - ): + if len(item.serial_no.split("\n")) != item.picked_qty: frappe.throw( _( - "You are picking more than required quantity for {}. Check if there is any other pick list created for {}" - ).format(item_code, item.sales_order) + "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" + ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), + title=_("Quantity Mismatch"), ) - frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) + def on_submit(self): + self.update_bundle_picked_qty() + self.update_reference_qty() + self.update_sales_order_picking_status() + + def on_cancel(self): + self.update_bundle_picked_qty() + self.update_reference_qty() + self.update_sales_order_picking_status() + + def update_reference_qty(self): + packed_items = [] + so_items = [] + + for item in self.locations: + if item.product_bundle_item: + packed_items.append(item.sales_order_item) + elif item.sales_order_item: + so_items.append(item.sales_order_item) + + if packed_items: + self.update_packed_items_qty(packed_items) + + if so_items: + self.update_sales_order_item_qty(so_items) + + def update_packed_items_qty(self, packed_items): + picked_items = get_picked_items_qty(packed_items) + self.validate_picked_qty(picked_items) + + picked_qty = frappe._dict() + for d in picked_items: + picked_qty[d.sales_order_item] = d.picked_qty + + for packed_item in packed_items: + frappe.db.set_value( + "Packed Item", + packed_item, + "picked_qty", + flt(picked_qty.get(packed_item)), + update_modified=False, + ) + + def update_sales_order_item_qty(self, so_items): + picked_items = get_picked_items_qty(so_items) + self.validate_picked_qty(picked_items) + + picked_qty = frappe._dict() + for d in picked_items: + picked_qty[d.sales_order_item] = d.picked_qty + + for so_item in so_items: + frappe.db.set_value( + "Sales Order Item", + so_item, + "picked_qty", + flt(picked_qty.get(so_item)), + update_modified=False, + ) + + def update_sales_order_picking_status(self) -> None: + sales_orders = [] + for row in self.locations: + if row.sales_order and row.sales_order not in sales_orders: + sales_orders.append(row.sales_order) - @staticmethod - def update_sales_order_picking_status(sales_orders: Set[str]) -> None: for sales_order in sales_orders: - if sales_order: - frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status() + frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status() + + def validate_picked_qty(self, data): + over_delivery_receipt_allowance = 100 + flt( + frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + ) + + for row in data: + if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance: + frappe.throw( + _( + f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}." + ) + ) @frappe.whitelist() def set_item_locations(self, save=False): @@ -309,6 +346,31 @@ class PickList(Document): return int(flt(min(possible_bundles), precision or 6)) +def get_picked_items_qty(items) -> List[Dict]: + return frappe.db.sql( + f""" + SELECT + sales_order_item, + item_code, + sales_order, + SUM(stock_qty) AS stock_qty, + SUM(picked_qty) AS picked_qty + FROM + `tabPick List Item` + WHERE + sales_order_item IN ( + {", ".join(frappe.db.escape(d) for d in items)} + ) + AND docstatus = 1 + GROUP BY + sales_order_item, + sales_order + FOR UPDATE + """, + as_dict=1, + ) + + def validate_item_locations(pick_list): if not pick_list.locations: frappe.throw(_("Add items in the Item Locations table")) @@ -441,42 +503,30 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( item_code, from_warehouses, required_qty, company ): - warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" - batch_locations = frappe.db.sql( - """ - SELECT - sle.`warehouse`, - sle.`batch_no`, - SUM(sle.`actual_qty`) AS `qty` - FROM - `tabStock Ledger Entry` sle, `tabBatch` batch - WHERE - sle.batch_no = batch.name - and sle.`item_code`=%(item_code)s - and sle.`company` = %(company)s - and batch.disabled = 0 - and sle.is_cancelled=0 - and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s - {warehouse_condition} - GROUP BY - sle.`warehouse`, - sle.`batch_no`, - sle.`item_code` - HAVING `qty` > 0 - ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`, sle.`batch_no`, sle.`warehouse` - """.format( - warehouse_condition=warehouse_condition - ), - { # nosec - "item_code": item_code, - "company": company, - "today": today(), - "warehouses": from_warehouses, - }, - as_dict=1, + sle = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + query = ( + frappe.qb.from_(sle) + .from_(batch) + .select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty")) + .where( + (sle.batch_no == batch.name) + & (sle.item_code == item_code) + & (sle.company == company) + & (batch.disabled == 0) + & (sle.is_cancelled == 0) + & (IfNull(batch.expiry_date, "2200-01-01") > today()) + ) + .groupby(sle.warehouse, sle.batch_no, sle.item_code) + .having(Sum(sle.actual_qty) > 0) + .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) ) - return batch_locations + if from_warehouses: + query = query.where(sle.warehouse.isin(from_warehouses)) + + return query.run(as_dict=True) def get_available_item_locations_for_serial_and_batched_item( diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 92e57bed220..7fbcbafbac1 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -1,7 +1,10 @@ def get_data(): return { "fieldname": "pick_list", + "internal_links": { + "Sales Order": ["locations", "sales_order"], + }, "transactions": [ - {"items": ["Stock Entry", "Delivery Note"]}, + {"items": ["Stock Entry", "Sales Order", "Delivery Note"]}, ], } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f7fcb30acd2..5af144110f0 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -236,8 +236,10 @@ def validate_item_details(args, item): validate_end_of_life(item.name, item.end_of_life, item.disabled) - if args.transaction_type == "selling" and cint(item.has_variants): - throw(_("Item {0} is a template, please select one of its variants").format(item.name)) + if cint(item.has_variants): + msg = f"Item {item.name} is a template, please select one of its variants" + + throw(_(msg), title=_("Template Item Selected")) elif args.transaction_type == "buying" and args.doctype != "Material Request": if args.get("is_subcontracted"): @@ -1181,7 +1183,7 @@ def get_projected_qty(item_code, warehouse): @frappe.whitelist() def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): - bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0} + bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} if warehouse: from frappe.query_builder.functions import Coalesce, Sum @@ -1197,7 +1199,6 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"), - Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"), ) .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses))) ).run(as_dict=True)[0] diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index e8faa4868f2..f4fd4de169d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -262,15 +262,17 @@ class SubcontractingReceipt(SubcontractingController): def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map + if not erpnext.is_perpetual_inventory_enabled(self.company): + return [] + gl_entries = [] self.make_item_gl_entries(gl_entries, warehouse_account) return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): - if erpnext.is_perpetual_inventory_enabled(self.company): - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + stock_rbnb = self.get_company_default("stock_received_but_not_billed") + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") warehouse_with_no_account = []