diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 7bf3826e781..31991648158 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -55,6 +55,8 @@ "post_change_gl_entries", "assets_tab", "asset_settings_section", + "calculate_depr_using_total_days", + "column_break_gjcc", "book_asset_depreciation_entry_automatically", "closing_settings_tab", "period_closing_settings_section", @@ -462,6 +464,17 @@ "fieldname": "enable_immutable_ledger", "fieldtype": "Check", "label": "Enable Immutable Ledger" + }, + { + "fieldname": "column_break_gjcc", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Enable this option to calculate daily depreciation by considering the total number of days in the entire depreciation period, (including leap years) while using daily pro-rata based depreciation", + "fieldname": "calculate_depr_using_total_days", + "fieldtype": "Check", + "label": "Calculate daily depreciation using total days in depreciation period" } ], "icon": "icon-cog", @@ -469,7 +482,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-05-11 23:19:44.673975", + "modified": "2024-07-12 00:24:20.957726", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -498,4 +511,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 34f0f24047b..93ff1e207c9 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -33,6 +33,7 @@ class AccountsSettings(Document): book_deferred_entries_based_on: DF.Literal["Days", "Months"] book_deferred_entries_via_journal_entry: DF.Check book_tax_discount_loss: DF.Check + calculate_depr_using_total_days: DF.Check check_supplier_invoice_uniqueness: DF.Check credit_controller: DF.Link | None delete_linked_ledger_entries: DF.Check diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 5bfb65a3138..cf0aae96260 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -481,6 +481,43 @@ class TestJournalEntry(unittest.TestCase): for field in self.fields: self.assertEqual(self.expected_gle[i][field], gl_entries[i][field]) + def test_negative_debit_and_credit_with_same_account_head(self): + from erpnext.accounts.general_ledger import process_gl_map + + # Create JV with defaut cost center - _Test Cost Center + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) + + jv = make_journal_entry("_Test Bank - _TC", "_Test Bank - _TC", 100 * -1, save=True) + jv.append( + "accounts", + { + "account": "_Test Cash - _TC", + "debit": 100 * -1, + "credit": 100 * -1, + "debit_in_account_currency": 100 * -1, + "credit_in_account_currency": 100 * -1, + "exchange_rate": 1, + }, + ) + jv.flags.ignore_validate = True + jv.save() + + self.assertEqual(len(jv.accounts), 3) + + gl_map = jv.build_gl_map() + + for row in gl_map: + if row.account == "_Test Cash - _TC": + self.assertEqual(row.debit_in_account_currency, 100 * -1) + self.assertEqual(row.credit_in_account_currency, 100 * -1) + + gl_map = process_gl_map(gl_map, False) + + for row in gl_map: + if row.account == "_Test Cash - _TC": + self.assertEqual(row.debit_in_account_currency, 100) + self.assertEqual(row.credit_in_account_currency, 100) + def make_journal_entry( account1, diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index e75057c7a7f..9bc110d243e 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -136,18 +136,28 @@ class PeriodClosingVoucher(AccountsController): def check_if_previous_year_closed(self): last_year_closing = add_days(self.year_start_date, -1) - previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True) + if not previous_fiscal_year: + return - if previous_fiscal_year and not frappe.db.exists( + previous_fiscal_year_start_date = previous_fiscal_year[0][1] + if not frappe.db.exists( "GL Entry", - {"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0}, + { + "posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]), + "company": self.company, + "is_cancelled": 0, + }, ): return - if previous_fiscal_year and not frappe.db.exists( + if not frappe.db.exists( "Period Closing Voucher", - {"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company}, + { + "posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]), + "docstatus": 1, + "company": self.company, + }, ): frappe.throw(_("Previous Year is not closed, please close it first")) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index c7505ce007d..1df21b7ebc4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -505,6 +505,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( return this.frm.call({ doc: me.frm.doc, method: "set_missing_values", + args: { + for_validate: true, + }, callback: function (r) { if (!r.exc) { if (r.message && r.message.print_format) { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 2fd7b5d3c81..a4d128a5845 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -310,6 +310,18 @@ def check_if_in_list(gle, gl_map): def toggle_debit_credit_if_negative(gl_map): for entry in gl_map: # toggle debit, credit if negative entry + if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit): + entry.credit *= -1 + entry.debit *= -1 + + if ( + flt(entry.debit_in_account_currency) < 0 + and flt(entry.credit_in_account_currency) < 0 + and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency) + ): + entry.credit_in_account_currency *= -1 + entry.debit_in_account_currency *= -1 + if flt(entry.debit) < 0: entry.credit = flt(entry.credit) - flt(entry.debit) entry.debit = 0.0 diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index f5c9d16073e..43856bf569f 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): +class TestAccountsPayable(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py new file mode 100644 index 00000000000..4dfdf3058e4 --- /dev/null +++ b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py @@ -0,0 +1,65 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import getdate, today + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_invoice(self, do_not_submit=False): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def test_basic_report_output(self): + si = self.create_sales_invoice() + + filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) + report = execute(filters) + + self.assertEqual(len(report[1]), 1) + + expected_result = { + "item_code": si.items[0].item_code, + "invoice": si.name, + "posting_date": getdate(), + "customer": si.customer, + "debit_to": si.debit_to, + "company": self.company, + "income_account": si.items[0].income_account, + "stock_qty": 1.0, + "stock_uom": si.items[0].stock_uom, + "rate": 100.0, + "amount": 100.0, + "total_tax": 0, + "total_other_charges": 0, + "total": 100.0, + "currency": "INR", + } + + report_output = {k: v for k, v in report[1][0].items() if k in expected_result} + self.assertDictEqual(report_output, expected_result) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index bd67a173343..c533a634a5b 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -701,20 +701,57 @@ def get_straight_line_or_manual_depr_amount( def get_daily_prorata_based_straight_line_depr( asset, row, schedule_idx, number_of_pending_depreciations, amount ): - total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12 - every_year_depr = amount / total_years + daily_depr_amount = get_daily_depr_amount(asset, row, schedule_idx, amount) - year_start_date = add_years( - row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12 - ) - year_end_date = add_days(add_years(year_start_date, 1), -1) - daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1) from_date, total_depreciable_days = _get_total_days( row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation ) return daily_depr_amount * total_depreciable_days +def get_daily_depr_amount(asset, row, schedule_idx, amount): + if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")): + total_days = ( + date_diff( + get_last_day( + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.opening_number_of_booked_depreciations + - 1 + ) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), + 1, + ), + ) + + 1 + ) + + return amount / total_days + else: + total_years = ( + flt( + (row.total_number_of_depreciations - row.total_number_of_booked_depreciations) + * row.frequency_of_depreciation + ) + / 12 + ) + + every_year_depr = amount / total_years + + year_start_date = add_years( + row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12 + ) + year_end_date = add_days(add_years(year_start_date, 1), -1) + + return every_year_depr / (date_diff(year_end_date, year_start_date) + 1) + + def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx): if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: return ( diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py index c359715571e..107d38057a2 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py @@ -75,6 +75,68 @@ class TestAssetDepreciationSchedule(FrappeTestCase): ] self.assertEqual(schedules, expected_schedules) + # Enable Checkbox to Calculate depreciation using total days in depreciation period + def test_daily_prorata_based_depr_after_enabling_configuration(self): + frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1) + + asset = create_asset( + calculate_depreciation=1, + depreciation_method="Straight Line", + daily_prorata_based=1, + gross_purchase_amount=1096, + available_for_use_date="2020-01-15", + depreciation_start_date="2020-01-31", + frequency_of_depreciation=1, + total_number_of_depreciations=36, + ) + + expected_schedule = [ + ["2020-01-31", 17.0, 17.0], + ["2020-02-29", 29.0, 46.0], + ["2020-03-31", 31.0, 77.0], + ["2020-04-30", 30.0, 107.0], + ["2020-05-31", 31.0, 138.0], + ["2020-06-30", 30.0, 168.0], + ["2020-07-31", 31.0, 199.0], + ["2020-08-31", 31.0, 230.0], + ["2020-09-30", 30.0, 260.0], + ["2020-10-31", 31.0, 291.0], + ["2020-11-30", 30.0, 321.0], + ["2020-12-31", 31.0, 352.0], + ["2021-01-31", 31.0, 383.0], + ["2021-02-28", 28.0, 411.0], + ["2021-03-31", 31.0, 442.0], + ["2021-04-30", 30.0, 472.0], + ["2021-05-31", 31.0, 503.0], + ["2021-06-30", 30.0, 533.0], + ["2021-07-31", 31.0, 564.0], + ["2021-08-31", 31.0, 595.0], + ["2021-09-30", 30.0, 625.0], + ["2021-10-31", 31.0, 656.0], + ["2021-11-30", 30.0, 686.0], + ["2021-12-31", 31.0, 717.0], + ["2022-01-31", 31.0, 748.0], + ["2022-02-28", 28.0, 776.0], + ["2022-03-31", 31.0, 807.0], + ["2022-04-30", 30.0, 837.0], + ["2022-05-31", 31.0, 868.0], + ["2022-06-30", 30.0, 898.0], + ["2022-07-31", 31.0, 929.0], + ["2022-08-31", 31.0, 960.0], + ["2022-09-30", 30.0, 990.0], + ["2022-10-31", 31.0, 1021.0], + ["2022-11-30", 30.0, 1051.0], + ["2022-12-31", 31.0, 1082.0], + ["2023-01-15", 14.0, 1096.0], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in get_depr_schedule(asset.name, "Draft") + ] + self.assertEqual(schedules, expected_schedule) + frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0) + # Test for Written Down Value Method # Frequency of deprciation = 3 def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 717b1c31026..c20eadeb78d 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -289,6 +289,11 @@ def repost(doc): if isinstance(message, dict): message = message.get("message") + status = "Failed" + # If failed because of timeout, set status to In Progress + if traceback and "timeout" in traceback.lower(): + status = "In Progress" + if traceback: message += "

" + "Traceback:
" + traceback @@ -297,7 +302,7 @@ def repost(doc): doc.name, { "error_log": message, - "status": "Failed", + "status": status, }, ) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 96cac9c06b2..6606a4f11a0 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -40,32 +40,40 @@ frappe.ui.form.on("Warehouse", { if (!frm.is_new()) { frappe.contacts.render_address_and_contact(frm); - let enable_toggle = frm.doc.disabled ? "Enable" : "Disable"; - frm.add_custom_button(__(enable_toggle), () => { - frm.set_value("disabled", 1 - frm.doc.disabled); - frm.save(); - }); - - frm.add_custom_button(__("Stock Balance"), function () { - frappe.set_route("query-report", "Stock Balance", { - warehouse: frm.doc.name, - company: frm.doc.company, + if (frm.has_perm("write")) { + let enable_toggle = frm.doc.disabled ? "Enable" : "Disable"; + frm.add_custom_button(__(enable_toggle), () => { + frm.set_value("disabled", 1 - frm.doc.disabled); + frm.save(); }); - }); - frm.add_custom_button( - frm.doc.is_group - ? __("Convert to Ledger", null, "Warehouse") - : __("Convert to Group", null, "Warehouse"), - function () { - convert_to_group_or_ledger(frm); - } - ); + frm.add_custom_button( + frm.doc.is_group + ? __("Convert to Ledger", null, "Warehouse") + : __("Convert to Group", null, "Warehouse"), + function () { + convert_to_group_or_ledger(frm); + } + ); + } + + if ("Stock Balance" in frappe.boot.user.all_reports) { + frm.add_custom_button(__("Stock Balance"), function () { + frappe.set_route("query-report", "Stock Balance", { + warehouse: frm.doc.name, + company: frm.doc.company, + }); + }); + } } else { frappe.contacts.clear_address_and_contact(frm); } - if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { + if ( + !frm.doc.is_group && + frm.doc.__onload?.account && + "General Ledger" in frappe.boot.user.all_reports + ) { frm.add_custom_button(__("General Ledger", null, "Warehouse"), function () { frappe.route_options = { account: frm.doc.__onload.account, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ff4798515c3..e14b0f87501 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -267,6 +267,8 @@ def repost_future_sle( "posting_time": args[i].get("posting_time"), "creation": args[i].get("creation"), "distinct_item_warehouses": distinct_item_warehouses, + "items_to_be_repost": args, + "current_index": i, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, @@ -685,11 +687,20 @@ class update_entries_after: self.distinct_item_warehouses[key] = val self.new_items_found = True else: + # Check if the dependent voucher is reposted + # If not, then do not add it to the list + if not self.is_dependent_voucher_reposted(dependant_sle): + return + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key) - if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set( + dependent_voucher_detail_nos + ): + return + val.sle_changed = True dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) val.dependent_voucher_detail_nos = dependent_voucher_detail_nos @@ -703,6 +714,27 @@ class update_entries_after: val.dependent_voucher_detail_nos = dependent_voucher_detail_nos self.distinct_item_warehouses[key] = val + def is_dependent_voucher_reposted(self, dependant_sle) -> bool: + # Return False if the dependent voucher is not reposted + + if self.args.items_to_be_repost and self.args.current_index: + index = self.args.current_index + while index < len(self.args.items_to_be_repost): + if ( + self.args.items_to_be_repost[index].get("item_code") == dependant_sle.item_code + and self.args.items_to_be_repost[index].get("warehouse") == dependant_sle.warehouse + ): + if getdate(self.args.items_to_be_repost[index].get("posting_date")) > getdate( + dependant_sle.posting_date + ): + self.args.items_to_be_repost[index]["posting_date"] = dependant_sle.posting_date + + return False + + index += 1 + + return True + def get_dependent_voucher_detail_nos(self, key): if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]: self.distinct_item_warehouses[key].dependent_voucher_detail_nos = [] diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 4ed73805314..c4eea3fda45 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -31,6 +31,22 @@ frappe.ui.form.on("Subcontracting Order", { }; }); + frm.set_query("cost_center", (doc) => { + return { + filters: { + company: doc.company, + }, + }; + }); + + frm.set_query("cost_center", "items", (doc) => { + return { + filters: { + company: doc.company, + }, + }; + }); + frm.set_query("set_warehouse", () => { return { filters: { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 8dfd9bd486d..b4a127702e0 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -174,6 +174,22 @@ frappe.ui.form.on("Subcontracting Receipt", { }; }); + frm.set_query("cost_center", (doc) => { + return { + filters: { + company: doc.company, + }, + }; + }); + + frm.set_query("cost_center", "items", (doc) => { + return { + filters: { + company: doc.company, + }, + }; + }); + frm.set_query("supplier_warehouse", () => { return { filters: {