diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 1a572d9823e..78c35266542 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -99,7 +99,7 @@ class BankClearance(Document): .where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .orderby(loan_disbursement.disbursement_date) - .orderby(loan_disbursement.name, frappe.qb.desc) + .orderby(loan_disbursement.name, order=frappe.qb.desc) ).run(as_dict=1) loan_repayment = frappe.qb.DocType("Loan Repayment") @@ -126,7 +126,9 @@ class BankClearance(Document): if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) - query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc) + query = query.orderby(loan_repayment.posting_date).orderby( + loan_repayment.name, order=frappe.qb.desc + ) loan_repayments = query.run(as_dict=True) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2ee356aaf40..2f3516e135a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -186,8 +186,10 @@ { "fetch_from": "bank_account.bank", "fieldname": "bank", - "fieldtype": "Read Only", - "label": "Bank" + "fieldtype": "Link", + "label": "Bank", + "options": "Bank", + "read_only": 1 }, { "fetch_from": "bank_account.bank_account_no", @@ -366,10 +368,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-18 12:24:14.178853", + "modified": "2022-09-30 16:19:43.680025", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -401,5 +404,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 986fc038c60..3020e6dc6e3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -98,7 +98,6 @@ "section_break_44", "apply_discount_on", "base_discount_amount", - "additional_discount_account", "column_break_46", "additional_discount_percentage", "discount_amount", @@ -1387,12 +1386,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "additional_discount_account", - "fieldtype": "Link", - "label": "Additional Discount Account", - "options": "Account" - }, { "default": "0", "fieldname": "ignore_default_payment_terms_template", @@ -1445,7 +1438,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-09-13 23:39:54.525037", + "modified": "2022-09-27 11:07:55.766844", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d1853002891..2b633cb8c34 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -669,9 +669,6 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" @@ -1159,9 +1156,6 @@ class PurchaseInvoice(BuyingController): def make_tax_gl_entries(self, gl_entries): # tax table gl entries valuation_tax = {} - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) for tax in self.get("taxes"): amount, base_amount = self.get_tax_amounts(tax, None) @@ -1249,15 +1243,6 @@ class PurchaseInvoice(BuyingController): ) ) - @property - def enable_discount_accounting(self): - if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) - - return self._enable_discount_accounting - def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 7fa2fe2a668..fca7e3a8873 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -74,7 +74,6 @@ "manufacturer_part_no", "accounting", "expense_account", - "discount_account", "col_break5", "is_fixed_asset", "asset_location", @@ -860,12 +859,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "discount_account", - "fieldtype": "Link", - "label": "Discount Account", - "options": "Account" - }, { "fieldname": "product_bundle", "fieldtype": "Link", @@ -877,7 +870,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:31:10.520171", + "modified": "2022-09-27 10:54:23.980713", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 94db9a9adc4..afd5a59df4e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -22,9 +22,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.party import get_due_date, get_party_account, get_party_details from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController @@ -1086,18 +1089,20 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", None) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_disposal(asset) - self.reset_depreciation_schedule(asset) + posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + reverse_depreciation_entry_made_after_disposal(asset, posting_date) + reset_depreciation_schedule(asset, self.posting_date) else: + if asset.calculate_depreciation: + depreciate_asset(asset, self.posting_date) + asset.reload() + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", self.posting_date) - if asset.calculate_depreciation: - self.depreciate_asset(asset) - for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 782e08e33ba..ce44ae304b3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -3196,6 +3196,37 @@ class TestSalesInvoice(unittest.TestCase): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) + def test_batch_expiry_for_sales_invoice_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pr = make_purchase_receipt(qty=1, item_code=item.name) + + batch_no = pr.items[0].batch_no + si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) + + si.load_from_db() + batch_no = si.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_si = make_return_doc(si.doctype, si.name) + return_si.save().submit() + + self.assertTrue(return_si.docstatus == 1) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -3289,6 +3320,7 @@ def create_sales_invoice(**args): "serial_no": args.serial_no, "conversion_factor": 1, "incoming_rate": args.incoming_rate or 0, + "batch_no": args.batch_no or None, }, ) 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 0b5df9e0cc0..84c2c9a3c3e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -335,6 +335,9 @@ def get_advance_vouchers( "party": ["in", parties], } + if party_type == "Customer": + filters.update({"against_voucher": ["is", "not set"]}) + if company: filters["company"] = company if from_date and to_date: diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 33bd3c74965..06e3c6120de 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -370,7 +370,7 @@ def get_conditions(filters): where parent=`tabSales Invoice`.name and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") + conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("cost_center") conditions += get_sales_invoice_item_field_condition("warehouse") conditions += get_sales_invoice_item_field_condition("brand") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c5eb7d8733f..9ede67848dd 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -86,7 +86,7 @@ def get_fiscal_years( ) ) - query = query.orderby(FY.year_start_date, Order.desc) + query = query.orderby(FY.year_start_date, order=Order.desc) fiscal_years = query.run(as_dict=True) frappe.cache().hset("fiscal_years", company, fiscal_years) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 991df4eada6..f0505ff9835 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -388,7 +388,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", + "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized", "read_only": 1 }, { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8ac7ed6387b..ca6be9b57b2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -828,7 +828,9 @@ class Asset(AccountsController): def update_maintenance_status(): - assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1}) + assets = frappe.get_all( + "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")} + ) for asset in assets: asset = frappe.get_doc("Asset", asset.name) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a4bb960d8e1..97941706aa8 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,11 +4,12 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate, today +from frappe.utils import add_months, cint, flt, getdate, nowdate, today from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) +from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry def post_depreciation_entries(date=None, commit=True): @@ -196,6 +197,11 @@ def scrap_asset(asset_name): _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) ) + date = today() + + depreciate_asset(asset, date) + asset.reload() + depreciation_series = frappe.get_cached_value( "Company", asset.company, "series_for_depreciation_entry" ) @@ -203,7 +209,7 @@ def scrap_asset(asset_name): je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" je.naming_series = depreciation_series - je.posting_date = today() + je.posting_date = date je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) @@ -214,7 +220,7 @@ def scrap_asset(asset_name): je.flags.ignore_permissions = True je.submit() - frappe.db.set_value("Asset", asset_name, "disposal_date", today()) + frappe.db.set_value("Asset", asset_name, "disposal_date", date) frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) asset.set_status("Scrapped") @@ -225,6 +231,9 @@ def scrap_asset(asset_name): def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) + reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) + reset_depreciation_schedule(asset, asset.disposal_date) + je = asset.journal_entry_for_scrap asset.db_set("disposal_date", None) @@ -235,6 +244,91 @@ def restore_asset(asset_name): asset.set_status() +def depreciate_asset(asset, date): + asset.flags.ignore_validate_update_after_submit = True + asset.prepare_depreciation_data(date_of_disposal=date) + asset.save() + + make_depreciation_entry(asset.name, date) + + +def reset_depreciation_schedule(asset, date): + asset.flags.ignore_validate_update_after_submit = True + + # recreate original depreciation schedule of the asset + asset.prepare_depreciation_data(date_of_return=date) + + modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + +def modify_depreciation_schedule_for_asset_repairs(asset): + asset_repairs = frappe.get_all( + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc("Asset Repair", repair.name) + asset_repair.modify_depreciation_schedule() + asset.prepare_depreciation_data() + + +def reverse_depreciation_entry_made_after_disposal(asset, date): + row = -1 + finance_book = asset.get("schedules")[0].get("finance_book") + for schedule in asset.get("schedules"): + if schedule.finance_book != finance_book: + row = 0 + finance_book = schedule.finance_book + else: + row += 1 + + if schedule.schedule_date == date: + if not disposal_was_made_on_original_schedule_date( + asset, schedule, row, date + ) or disposal_happens_in_the_future(date): + + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True + reverse_journal_entry.submit() + + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) + asset.finance_books[0].value_after_depreciation += depreciation_amount + asset.save() + + +def get_depreciation_amount_in_je(journal_entry): + if journal_entry.accounts[0].debit_in_account_currency: + return journal_entry.accounts[0].debit_in_account_currency + else: + return journal_entry.accounts[0].credit_in_account_currency + + +# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone +def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal): + for finance_book in asset.get("finance_books"): + if schedule.finance_book == finance_book.finance_book: + orginal_schedule_date = add_months( + finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) + ) + + if orginal_schedule_date == posting_date_of_disposal: + return True + return False + + +def disposal_happens_in_the_future(posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + + def get_gl_entries_on_asset_regain( asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None ): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index e7af9bd5bc2..370b13bb98d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -4,10 +4,23 @@ import unittest import frappe -from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate +from frappe.utils import ( + add_days, + add_months, + cstr, + flt, + get_first_day, + get_last_day, + getdate, + nowdate, +) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset +from erpnext.assets.doctype.asset.asset import ( + make_sales_invoice, + split_asset, + update_maintenance_status, +) from erpnext.assets.doctype.asset.depreciation import ( post_depreciation_entries, restore_asset, @@ -178,28 +191,48 @@ class TestAsset(AssetSetup): self.assertEqual(doc.items[0].is_fixed_asset, 1) def test_scrap_asset(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-01-01", - purchase_date="2020-01-01", + available_for_use_date=purchase_date, + purchase_date=purchase_date, expected_value_after_useful_life=10000, total_number_of_depreciations=10, frequency_of_depreciation=1, submit=1, ) - post_depreciation_entries(date=add_months("2020-01-01", 4)) + post_depreciation_entries(date=add_months(purchase_date, 2)) + asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + self.assertEquals(accumulated_depr_amount, 18000.0) scrap_asset(asset.name) - asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + self.assertEquals(accumulated_depr_amount, 18000.00 + pro_rata_amount) + self.assertEqual(asset.status, "Scrapped") self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0), ) gle = frappe.db.sql( @@ -216,7 +249,64 @@ class TestAsset(AssetSetup): self.assertFalse(asset.journal_entry_for_scrap) self.assertEqual(asset.status, "Partially Depreciated") + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + this_month_depr_amount = 9000.0 if get_last_day(date) == date else 0 + + self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount) + def test_gle_made_by_asset_sale(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date=purchase_date, + purchase_date=purchase_date, + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + post_depreciation_entries(date=add_months(purchase_date, 2)) + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0), + ("Debtors - _TC", 25000.0, 0.0), + ) + + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", + si.name, + ) + + self.assertSequenceEqual(gle, expected_gle) + + si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + + def test_asset_with_maintenance_required_status_after_sale(self): asset = create_asset( calculate_depreciation=1, available_for_use_date="2020-06-06", @@ -224,6 +314,7 @@ class TestAsset(AssetSetup): expected_value_after_useful_life=10000, total_number_of_depreciations=3, frequency_of_depreciation=10, + maintenance_required=1, depreciation_start_date="2020-12-31", submit=1, ) @@ -239,24 +330,9 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), - ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), - ("Debtors - _TC", 25000.0, 0.0), - ) + update_maintenance_status() - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - - self.assertSequenceEqual(gle, expected_gle) - - si.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") def test_asset_splitting(self): asset = create_asset( @@ -1376,6 +1452,7 @@ def create_asset(**args): "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, "gross_purchase_amount": args.gross_purchase_amount or 100000, "purchase_receipt_amount": args.purchase_receipt_amount or 100000, + "maintenance_required": args.maintenance_required or 0, "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": args.available_for_use_date or "2020-06-06", "location": args.location or "Test Location", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 93194c0333a..08355f047e5 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -12,8 +12,11 @@ from six import string_types import erpnext from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_gl_entries_on_asset_disposal, get_value_after_depreciation_on_disposal_date, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( @@ -424,7 +427,7 @@ class AssetCapitalization(StockController): asset = self.get_asset(item) if asset.calculate_depreciation: - self.depreciate_asset(asset) + depreciate_asset(asset, self.posting_date) asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( @@ -520,8 +523,8 @@ class AssetCapitalization(StockController): self.set_consumed_asset_status(asset) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_disposal(asset) - self.reset_depreciation_schedule(asset) + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + reset_depreciation_schedule(asset, self.posting_date) def get_asset(self, item): asset = frappe.get_doc("Asset", item.asset) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index aad26075f2d..28158a31b94 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -20,7 +20,6 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", - "enable_discount_accounting", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -134,13 +133,6 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", - "fieldtype": "Check", - "label": "Enable Discount Accounting for Buying" } ], "icon": "fa fa-cog", @@ -148,7 +140,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-09-01 18:01:34.994657", + "modified": "2022-09-27 10:50:27.050252", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 7b18cdbedcd..be1ebdeb64e 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -5,15 +5,10 @@ import frappe -from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document -from frappe.utils import cint class BuyingSettings(Document): - def on_update(self): - self.toggle_discount_accounting_fields() - def validate(self): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) @@ -26,60 +21,3 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) - - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index fc99d776d4a..ddf81ca3aec 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -33,6 +33,7 @@ frappe.ui.form.on("Purchase Order", { frm.set_query("fg_item", "items", function() { return { filters: { + 'is_stock_item': 1, 'is_sub_contracted_item': 1, 'default_bom': ['!=', ''] } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8686cb5cc09..22291a35441 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,7 +38,6 @@ from erpnext.accounts.party import ( validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year -from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -1891,88 +1890,6 @@ class AccountsController(TransactionBase): _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) ) - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(date_of_disposal=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, self.posting_date) - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc("Asset Repair", repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def reverse_depreciation_entry_made_after_disposal(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry() - - row = -1 - finance_book = asset.get("schedules")[0].get("finance_book") - for schedule in asset.get("schedules"): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_disposal: - if not self.disposal_was_made_on_original_schedule_date( - asset, schedule, row, posting_date_of_original_disposal - ) or self.disposal_happens_in_the_future(posting_date_of_original_disposal): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - frappe.flags.is_reverse_depr_entry = False - asset.flags.ignore_validate_update_after_submit = True - schedule.journal_entry = None - asset.save() - - def get_posting_date_of_disposal_entry(self): - if self.doctype == "Sales Invoice" and self.return_against: - return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") - else: - return self.posting_date - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def disposal_was_made_on_original_schedule_date( - self, asset, schedule, row, posting_date_of_disposal - ): - for finance_book in asset.get("finance_books"): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months( - finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) - ) - - if orginal_schedule_date == posting_date_of_disposal: - return True - return False - - def disposal_happens_in_the_future(self, posting_date_of_disposal): - if posting_date_of_disposal > getdate(): - return True - - return False - @frappe.whitelist() def get_tax_rate(account_head): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 4f8b5c79d24..8eae0a07028 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -212,21 +212,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() - # these are handled separately - ignored_search_fields = ("item_name", "description") - for ignored_field in ignored_search_fields: - if ignored_field in searchfields: - searchfields.remove(ignored_field) - columns = "" - extra_searchfields = [ - field - for field in searchfields - if not field in ["name", "item_group", "description", "item_name"] - ] + extra_searchfields = [field for field in searchfields if not field in ["name", "description"]] if extra_searchfields: - columns = ", " + ", ".join(extra_searchfields) + columns += ", " + ", ".join(extra_searchfields) + + if "description" in searchfields: + columns += """, if(length(tabItem.description) > 40, \ + concat(substr(tabItem.description, 1, 40), "..."), description) as description""" searchfields = searchfields + [ field @@ -266,12 +260,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if frappe.db.count(doctype, cache=True) < 50000: # scan description only if items are less than 50000 description_cond = "or tabItem.description LIKE %(txt)s" + return frappe.db.sql( """select - tabItem.name, tabItem.item_name, tabItem.item_group, - if(length(tabItem.description) > 40, \ - concat(substr(tabItem.description, 1, 40), "..."), description) as description - {columns} + tabItem.name {columns} from tabItem where tabItem.docstatus < 2 and tabItem.disabled=0 diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 202a880750e..aa4468c04e4 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -69,9 +69,18 @@ class SubcontractingController(StockController): def validate_items(self): for item in self.items: - if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): + is_stock_item, is_sub_contracted_item = frappe.get_value( + "Item", item.item_code, ["is_stock_item", "is_sub_contracted_item"] + ) + + if not is_stock_item: + msg = f"Item {item.item_name} must be a stock item." + frappe.throw(_(msg)) + + if not is_sub_contracted_item: msg = f"Item {item.item_name} must be a subcontracted item." frappe.throw(_(msg)) + if item.bom: bom = frappe.get_doc("BOM", item.bom) if not bom.is_active: @@ -841,7 +850,7 @@ def make_rm_stock_entry( for fg_item_code in fg_item_code_list: for rm_item in rm_items: - if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code: + if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code: rm_item_code = rm_item.get("rm_item_code") items_dict = { diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6ef77f3f5ba..b8f51f839ce 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -508,6 +508,7 @@ accounting_dimension_doctypes = [ "Landed Cost Item", "Asset Value Adjustment", "Asset Repair", + "Asset Capitalization", "Loyalty Program", "Stock Reconciliation", "POS Profile", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2a0ca8c4961..fc63f124e15 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,3 +315,4 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index a1d40b739eb..0bd3fcdec4c 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -100,6 +100,7 @@ def execute(): "mode_of_payment": loan.mode_of_payment, "loan_account": loan.loan_account, "payment_account": loan.payment_account, + "disbursement_account": loan.payment_account, "interest_income_account": loan.interest_income_account, "penalty_income_account": loan.penalty_income_account, }, @@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account): loan_type_doc.company = loan.company loan_type_doc.mode_of_payment = loan.mode_of_payment loan_type_doc.payment_account = loan.payment_account + loan_type_doc.disbursement_account = loan.payment_account loan_type_doc.loan_account = loan.loan_account loan_type_doc.interest_income_account = loan.interest_income_account loan_type_doc.penalty_income_account = penalty_account diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py new file mode 100644 index 00000000000..09e20a9d794 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py @@ -0,0 +1,31 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + doctype = "Asset Capitalization" + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/public/scss/order-page.scss b/erpnext/public/scss/order-page.scss new file mode 100644 index 00000000000..6f5fe5d4d7a --- /dev/null +++ b/erpnext/public/scss/order-page.scss @@ -0,0 +1,115 @@ +#page-order { + .main-column { + .page-content-wrapper { + + .breadcrumb-container { + @media screen and (min-width: 567px) { + padding-left: var(--padding-sm); + } + } + + .container.my-4 { + background-color: var(--fg-color); + + @media screen and (min-width: 567px) { + padding: 1.25rem 1.5rem; + border-radius: var(--border-radius-md); + box-shadow: var(--card-shadow); + } + } + } + } +} + +.indicator-container { + @media screen and (max-width: 567px) { + padding-bottom: 0.8rem; + } +} + +.order-items { + padding: 1.5rem 0; + border-bottom: 1px solid var(--border-color); + color: var(--gray-700); + + @media screen and (max-width: 567px) { + align-items: flex-start !important; + } + .col-2 { + @media screen and (max-width: 567px) { + flex: auto; + max-width: 28%; + } + } + + .order-item-name { + font-size: var(--text-base); + font-weight: 500; + } + + .btn:focus, + .btn:hover { + background-color: var(--control-bg); + } + + + .col-6 { + @media screen and (max-width: 567px) { + max-width: 100%; + } + + &.order-item-name { + font-size: var(--text-base); + } + } +} + +.item-grand-total { + font-size: var(--text-base); +} + +.list-item-name, +.item-total, +.order-container, +.order-qty { + font-size: var(--text-md); +} + +.d-s-n { + @media screen and (max-width: 567px) { + display: none; + } +} + +.d-l-n { + @media screen and (min-width: 567px) { + display: none; + } +} + +.border-btm { + border-bottom: 1px solid var(--border-color); +} + +.order-taxes { + display: flex; + + @media screen and (min-width: 567px) { + justify-content: flex-end; + } + + .col-4 { + padding-right: 0; + + .col-8 { + padding-left: 0; + padding-right: 0; + } + + @media screen and (max-width: 567px) { + padding-left: 0; + flex: auto; + max-width: 100%; + } + } +} \ No newline at end of file diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 9ea84160342..b5e97f1c34b 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -1,3 +1,4 @@ +@import './order-page'; .filter-options { max-height: 300px; @@ -32,19 +33,29 @@ height: 24px; } -.website-list .result { - margin-top: 2rem; -} +.website-list { + background-color: var(--fg-color); + padding: 0 var(--padding-lg); + border-radius: var(--border-radius-md); -.result { - border-bottom: 1px solid var(--border-color); + @media screen and (max-width: 567px) { + margin-left: -2rem; + } + + &.result { + border-bottom: 1px solid var(--border-color); + } } .transaction-list-item { padding: 1rem 0; - border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); position: relative; + &:only-child, &:last-child { + border: 0; + } + a.transaction-item-link { position: absolute; top: 0; @@ -68,3 +79,13 @@ line-height: 1.3; } } + +.list-item-name, .item-total { + font-size: var(--font-size-sm); +} + +.items-preview { + @media screen and (max-width: 567px) { + margin-top: 1rem; + } +} \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6bcab737b37..1b9f16814c5 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,7 +6,7 @@ import json import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on @@ -1091,6 +1091,36 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) ) + def test_batch_expiry_for_delivery_note(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt(qty=1, item_code=item.name) + + dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no) + + dn.load_from_db() + batch_no = dn.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save().submit() + + self.assertTrue(return_dn.docstatus == 1) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -1117,6 +1147,7 @@ def create_delivery_note(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, + "batch_no": args.batch_no or None, "target_warehouse": args.target_warehouse, }, ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 7e1476d240a..e61f0f514e3 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -10,6 +10,31 @@ frappe.ui.form.on("Item", { frm.add_fetch('attribute', 'to_range', 'to_range'); frm.add_fetch('attribute', 'increment', 'increment'); frm.add_fetch('tax_type', 'tax_rate', 'tax_rate'); + + frm.make_methods = { + 'Sales Order': () => { + open_form(frm, "Sales Order", "Sales Order Item", "items"); + }, + 'Delivery Note': () => { + open_form(frm, "Delivery Note", "Delivery Note Item", "items"); + }, + 'Sales Invoice': () => { + open_form(frm, "Sales Invoice", "Sales Invoice Item", "items"); + }, + 'Purchase Order': () => { + open_form(frm, "Purchase Order", "Purchase Order Item", "items"); + }, + 'Purchase Receipt': () => { + open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); + }, + 'Purchase Invoice': () => { + open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); + }, + 'Material Request': () => { + open_form(frm, "Material Request", "Material Request Item", "items"); + }, + }; + }, onload: function(frm) { erpnext.item.setup_queries(frm); @@ -858,3 +883,17 @@ frappe.tour['Item'] = [ ]; + +function open_form(frm, doctype, child_doctype, parentfield) { + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield); + new_child_doc.item_code = frm.doc.name; + new_child_doc.item_name = frm.doc.item_name; + new_child_doc.uom = frm.doc.stock_uom; + new_child_doc.description = frm.doc.description; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 143fe408c34..c8bb1b960eb 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -937,17 +937,21 @@ class Item(Document): "Purchase Order Item", "Material Request Item", "Product Bundle", + "BOM", ] for doctype in linked_doctypes: filters = {"item_code": self.name, "docstatus": 1} - if doctype == "Product Bundle": - filters = {"new_item_code": self.name} + if doctype in ("Product Bundle", "BOM"): + if doctype == "Product Bundle": + filters = {"new_item_code": self.name} + fieldname = "new_item_code as docname" + else: + filters = {"item": self.name, "docstatus": 1} + fieldname = "name as docname" - if linked_doc := frappe.db.get_value( - doctype, filters, ["new_item_code as docname"], as_dict=True - ): + if linked_doc := frappe.db.get_value(doctype, filters, fieldname, as_dict=True): return linked_doc.update({"doctype": doctype}) elif doctype in ( diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 1cee553be5b..e35c8bf335e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -5,6 +5,7 @@ import json import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_objects from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today @@ -816,6 +817,30 @@ class TestItem(FrappeTestCase): item.reload() self.assertEqual(item.is_stock_item, 1) + def test_serach_fields_for_item(self): + from erpnext.controllers.queries import item_query + + make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype") + + item = make_item(properties={"item_name": "Test Item", "description": "Test Description"}) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertTrue("description" not in data[0]) + + make_property_setter( + "Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype" + ) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertEqual(data[0].description, item.description) + self.assertTrue("description" in data[0]) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index adddb413824..9c1c7e56796 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -183,7 +183,7 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item - key = (item_code, item.uom, item.warehouse, reference) + key = (item_code, item.uom, item.warehouse, item.batch_no, reference) item.idx = None item.name = None diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index f7f8cbe4ee0..c64370dcdf2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,7 +153,9 @@ class StockLedgerEntry(Document): def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": - if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0: + if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or ( + self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0 + ): return expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html index c7f0d06dff2..0072dc280c7 100644 --- a/erpnext/templates/includes/footer/footer_extension.html +++ b/erpnext/templates/includes/footer/footer_extension.html @@ -6,7 +6,7 @@ aria-label="{{ _('Your email address...') }}" aria-describedby="footer-subscribe-button">
+ {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} +
{% endif %} - +- {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} -
- {% endif %} -
- {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
- {{ party_name }}
-
- {% if doc.contact_display and doc.contact_display != party_name %}
-
- {{ doc.contact_display }}
- {% endif %}
-
| - {{ _("Item") }} - | -- {{ _("Quantity") }} - | -- {{ _("Amount") }} - | - - - {% for d in doc.items %} -
|---|---|---|
| - {{ item_name_and_description(d) }} - | -
- {{ d.qty }}
- {% if d.delivered_qty is defined and d.delivered_qty != None %}
- {{ _("Delivered") }} {{ d.delivered_qty }} +
+
+
+ {% if doc.doctype == "Quotation" and not doc.docstatus %}
+ {{ _("Pending") }}
+ {% else %}
+ {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
{% endif %}
- |
-
- {{ d.get_formatted("amount") }}
- {{ _("Rate:") }} {{ d.get_formatted("rate") }} - |
-
Available Points: {{ available_loyalty_points }}
+Available Points: {{ + available_loyalty_points }}
{{ doc.terms }}
+{{ doc.terms }}