diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 1a572d9823..78c3526654 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/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7af41f398a..763e2e6992 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", { var update_jv_details = function(doc, r) { $.each(r, function(i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); - row.account = d.account; - row.balance = d.balance; + frappe.model.set_value(row.doctype, row.name, "account", d.account) + frappe.model.set_value(row.doctype, row.name, "balance", d.balance) }); refresh_field("accounts"); } diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2ee356aaf4..2f3516e135 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/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b126d57400..eedaaaf338 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -343,7 +343,8 @@ "no_copy": 1, "options": "POS Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -1553,7 +1554,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2022-03-22 13:00:24.166684", + "modified": "2022-09-30 03:49:50.455199", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 6e3a0766f1..54a3e934b2 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice): frappe.bold(d.warehouse), frappe.bold(d.qty), ) - if flt(available_stock) <= 0: + if is_stock_item and flt(available_stock) <= 0: frappe.throw( _("Row #{}: Item Code: {} is not available under warehouse {}.").format( d.idx, item_code, warehouse ), title=_("Item Unavailable"), ) - elif flt(available_stock) < flt(d.qty): + elif is_stock_item and flt(available_stock) < flt(d.qty): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." @@ -632,11 +632,12 @@ def get_stock_availability(item_code, warehouse): pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty, is_stock_item else: - is_stock_item = False + is_stock_item = True if frappe.db.exists("Product Bundle", item_code): return get_bundle_availability(item_code, warehouse), is_stock_item else: - # Is a service item + is_stock_item = False + # Is a service item or non_stock item return 0, is_stock_item @@ -650,7 +651,9 @@ def get_bundle_availability(bundle_item_code, warehouse): available_qty = item_bin_qty - item_pos_reserved_qty max_available_bundles = available_qty / item.qty - if bundle_bin_qty > max_available_bundles: + if bundle_bin_qty > max_available_bundles and frappe.get_value( + "Item", item.item_code, "is_stock_item" + ): bundle_bin_qty = max_available_bundles pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 986fc038c6..1e477776e2 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", @@ -1437,6 +1430,7 @@ "fieldname": "tax_withheld_vouchers", "fieldtype": "Table", "label": "Tax Withheld Vouchers", + "no_copy": 1, "options": "Tax Withheld Vouchers", "read_only": 1 } @@ -1445,7 +1439,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-09-13 23:39:54.525037", + "modified": "2022-10-07 14:19:14.214157", "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 d185300289..2b633cb8c3 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/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0a4f25b876..f901257ccf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1543,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.save() self.assertEqual(pi.items[0].conversion_factor, 1000) + def test_batch_expiry_for_purchase_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.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_invoice( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( 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 7fa2fe2a66..fca7e3a887 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 e51938b27f..afd5a59df4 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 @@ -1081,23 +1084,25 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) 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 + 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 782e08e33b..ce44ae304b 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 0b5df9e0cc..7eddd81ee0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -249,6 +249,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N ) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + + # once tds is deducted, not need to add vouchers in the invoice + voucher_wise_amount = {} else: tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) @@ -335,6 +338,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: @@ -422,7 +428,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0 + net_total = 0 + if vouchers: + net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") + net_total += inv.net_total supp_credit_amt = net_total - cumulative_threshold diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 33bd3c7496..06e3c6120d 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 c5eb7d8733..9ede67848d 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.js b/erpnext/assets/doctype/asset/asset.js index a43a16c9ec..5512d4159d 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -230,7 +230,7 @@ frappe.ui.form.on('Asset', { datasets: [{ color: 'green', values: asset_values, - formatted: asset_values.map(d => d.toFixed(2)) + formatted: asset_values.map(d => d?.toFixed(2)) }] }, type: 'line' diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 991df4eada..f0505ff983 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 8ac7ed6387..ca6be9b57b 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 74386384c5..97941706aa 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,7 +244,94 @@ def restore_asset(asset_name): asset.set_status() -def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): +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 +): ( fixed_asset_account, asset, @@ -247,28 +343,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "debit_in_account_currency": asset.gross_purchase_amount, - "debit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "credit_in_account_currency": accumulated_depr_amount, - "credit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries -def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_disposal( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -280,23 +393,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "credit_in_account_currency": asset.gross_purchase_amount, - "credit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "debit_in_account_currency": accumulated_depr_amount, - "debit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "credit_in_account_currency": asset.gross_purchase_amount, + "credit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "debit_in_account_currency": accumulated_depr_amount, + "debit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries @@ -333,15 +461,21 @@ def get_asset_details(asset, finance_book=None): ) -def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): +def get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center +): debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( - { - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount), - } + asset.get_gl_dict( + { + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount), + "posting_date": getdate(), + }, + item=asset, + ) ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index e7af9bd5bc..370b13bb98 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 2e6f0ad7b0..08355f047e 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,11 +427,15 @@ 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( - asset, item.asset_value, item.get("finance_book") or self.get("finance_book") + asset, + item.asset_value, + item.get("finance_book") or self.get("finance_book"), + self.get("doctype"), + self.get("name"), ) asset.db_set("disposal_date", self.posting_date) @@ -516,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 aad26075f2..28158a31b9 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 7b18cdbedc..be1ebdeb64 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 fc99d776d4..ddf81ca3ae 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 8686cb5cc0..22291a3544 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 4f8b5c79d2..8eae0a0702 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 202a880750..aa4468c04e 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/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index c5775ee907..6556eabf4a 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -188,7 +188,8 @@ "in_list_view": 1, "label": "Item Group", "options": "Item Group", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "1", @@ -234,7 +235,8 @@ "fieldname": "brand", "fieldtype": "Link", "label": "Brand", - "options": "Brand" + "options": "Brand", + "search_index": 1 }, { "collapsible": 1, @@ -346,7 +348,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-09-13 04:05:11.614087", + "modified": "2022-09-30 04:01:52.090732", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index c0f8c79283..3e5d5f768f 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -403,9 +403,6 @@ def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Website Item", ["route(500)"]) - frappe.db.add_index("Website Item", ["item_group"]) - frappe.db.add_index("Website Item", ["brand"]) - def check_if_user_is_customer(user=None): from frappe.contacts.doctype.contact.contact import get_contact_name diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a08feb4447..b8f51f839c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -391,12 +391,12 @@ scheduler_events = { "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", ], "hourly": [ - "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", ], "hourly_long": [ + "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", ], @@ -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/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index a53c42c5ec..804f03dc51 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -17,6 +17,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( close_work_order, make_job_card, make_stock_entry, + make_stock_return_entry, stop_unstop, ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -1408,6 +1409,77 @@ class TestWorkOrder(FrappeTestCase): ) self.assertEqual(manufacture_ste_doc2.items[1].qty, 1) + def test_non_consumed_material_return_against_work_order(self): + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + + item = make_item( + "Test FG Item To Test Return Case", + { + "is_stock_item": 1, + }, + ) + + item_code = item.name + bom_doc = make_bom( + item=item_code, + source_warehouse="Stores - _TC", + raw_materials=["Test Batch MCC Keyboard", "Test Serial No BTT Headphone"], + ) + + # Create a work order + wo_doc = make_wo_order_test_record(production_item=item_code, qty=5) + wo_doc.save() + + self.assertEqual(wo_doc.bom_no, bom_doc.name) + + # Transfer material for manufacture + ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 5)) + for row in ste_doc.items: + row.qty += 2 + row.transfer_qty += 2 + nste_doc = test_stock_entry.make_stock_entry( + item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100 + ) + + row.batch_no = nste_doc.items[0].batch_no + row.serial_no = nste_doc.items[0].serial_no + + ste_doc.save() + ste_doc.submit() + ste_doc.load_from_db() + + # Create a stock entry to manufacture the item + ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5)) + for row in ste_doc.items: + if row.s_warehouse and not row.t_warehouse: + row.qty -= 2 + row.transfer_qty -= 2 + + if row.serial_no: + serial_nos = get_serial_nos(row.serial_no) + row.serial_no = "\n".join(serial_nos[0:5]) + + ste_doc.save() + ste_doc.submit() + + wo_doc.load_from_db() + for row in wo_doc.required_items: + self.assertEqual(row.transferred_qty, 7) + self.assertEqual(row.consumed_qty, 5) + + self.assertEqual(wo_doc.status, "Completed") + return_ste_doc = make_stock_return_entry(wo_doc.name) + return_ste_doc.save() + + self.assertTrue(return_ste_doc.is_return) + for row in return_ste_doc.items: + self.assertEqual(row.qty, 2) + def prepare_data_for_backflush_based_on_materials_transferred(): batch_item_doc = make_item( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 20f15039ef..4aab3fa373 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -180,6 +180,37 @@ frappe.ui.form.on("Work Order", { frm.trigger("make_bom"); }); } + + frm.trigger("add_custom_button_to_return_components"); + }, + + add_custom_button_to_return_components: function(frm) { + if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) { + let non_consumed_items = frm.doc.required_items.filter(d =>{ + return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty) + }); + + if (non_consumed_items && non_consumed_items.length) { + frm.add_custom_button(__("Return Components"), function() { + frm.trigger("create_stock_return_entry"); + }).addClass("btn-primary"); + } + } + }, + + create_stock_return_entry: function(frm) { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry", + args: { + "work_order": frm.doc.name, + }, + callback: function(r) { + if(!r.exc) { + let doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); }, make_job_card: function(frm) { @@ -517,7 +548,8 @@ frappe.ui.form.on("Work Order Operation", { erpnext.work_order = { set_custom_buttons: function(frm) { var doc = frm.doc; - if (doc.docstatus === 1 && doc.status != "Closed") { + + if (doc.status !== "Closed") { frm.add_custom_button(__('Close'), function() { frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."), () => { @@ -525,7 +557,9 @@ erpnext.work_order = { } ); }, __("Status")); + } + if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) { if (doc.status != 'Stopped' && doc.status != 'Completed') { frm.add_custom_button(__('Stop'), function() { erpnext.work_order.change_work_order_status(frm, "Stopped"); @@ -557,37 +591,52 @@ erpnext.work_order = { if(!frm.doc.skip_transfer){ // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption - if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing)) - && frm.doc.status != 'Stopped') { - frm.has_finish_btn = true; + if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') { + if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) { + frm.has_finish_btn = true; - if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { - // Only show "Material Consumption" when required_qty > consumed_qty - var counter = 0; - var tbl = frm.doc.required_items || []; - var tbl_lenght = tbl.length; - for (var i = 0, len = tbl_lenght; i < len; i++) { - let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; - if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { - counter += 1; + if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { + // Only show "Material Consumption" when required_qty > consumed_qty + var counter = 0; + var tbl = frm.doc.required_items || []; + var tbl_lenght = tbl.length; + for (var i = 0, len = tbl_lenght; i < len; i++) { + let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { + counter += 1; + } + } + if (counter > 0) { + var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { + const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; + erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); + }); + consumption_btn.addClass('btn-primary'); } } - if (counter > 0) { - var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { - const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; - erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); - }); - consumption_btn.addClass('btn-primary'); + + var finish_btn = frm.add_custom_button(__('Finish'), function() { + erpnext.work_order.make_se(frm, 'Manufacture'); + }); + + if(doc.material_transferred_for_manufacturing>=doc.qty) { + // all materials transferred for manufacturing, make this primary + finish_btn.addClass('btn-primary'); } - } + } else { + frappe.db.get_doc("Manufacturing Settings").then((doc) => { + let allowance_percentage = doc.overproduction_percentage_for_work_order; - var finish_btn = frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); - }); + if (allowance_percentage > 0) { + let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); - if(doc.material_transferred_for_manufacturing>=doc.qty) { - // all materials transferred for manufacturing, make this primary - finish_btn.addClass('btn-primary'); + if ((flt(doc.produced_qty) < allowed_qty)) { + frm.add_custom_button(__('Finish'), function() { + erpnext.work_order.make_se(frm, 'Manufacture'); + }); + } + } + }); } } } else { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7b8625372a..1e6d982fc9 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -20,6 +20,7 @@ from frappe.utils import ( nowdate, time_diff_in_hours, ) +from pypika import functions as fn from erpnext.manufacturing.doctype.bom.bom import ( get_bom_item_rate, @@ -859,6 +860,7 @@ class WorkOrder(Document): if self.docstatus == 1: # calculate transferred qty based on submitted stock entries self.update_transferred_qty_for_required_items() + self.update_returned_qty() # update in bin self.update_reserved_qty_for_production() @@ -930,23 +932,62 @@ class WorkOrder(Document): self.set_available_qty() def update_transferred_qty_for_required_items(self): - """update transferred qty from submitted stock entries for that item against - the work order""" + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") - for d in self.required_items: - transferred_qty = frappe.db.sql( - """select sum(qty) - from `tabStock Entry` entry, `tabStock Entry Detail` detail - where - entry.work_order = %(name)s - and entry.purpose = 'Material Transfer for Manufacture' - and entry.docstatus = 1 - and detail.parent = entry.name - and (detail.item_code = %(item)s or detail.original_item = %(item)s)""", - {"name": self.name, "item": d.item_code}, - )[0][0] + query = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on((ste_child.parent == ste.name)) + .select( + ste_child.item_code, + ste_child.original_item, + fn.Sum(ste_child.qty).as_("qty"), + ) + .where( + (ste.docstatus == 1) + & (ste.work_order == self.name) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 0) + ) + .groupby(ste_child.item_code) + ) - d.db_set("transferred_qty", flt(transferred_qty), update_modified=False) + data = query.run(as_dict=1) or [] + transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data}) + + for row in self.required_items: + row.db_set( + "transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False + ) + + def update_returned_qty(self): + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on((ste_child.parent == ste.name)) + .select( + ste_child.item_code, + ste_child.original_item, + fn.Sum(ste_child.qty).as_("qty"), + ) + .where( + (ste.docstatus == 1) + & (ste.work_order == self.name) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 1) + ) + .groupby(ste_child.item_code) + ) + + data = query.run(as_dict=1) or [] + returned_dict = frappe._dict({d.original_item or d.item_code: d.qty for d in data}) + + for row in self.required_items: + row.db_set("returned_qty", (returned_dict.get(row.item_code) or 0.0), update_modified=False) def update_consumed_qty_for_required_items(self): """ @@ -1470,3 +1511,25 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: ) ) ).run()[0][0] or 0.0 + + +@frappe.whitelist() +def make_stock_return_entry(work_order): + from erpnext.stock.doctype.stock_entry.stock_entry import get_available_materials + + non_consumed_items = get_available_materials(work_order) + if not non_consumed_items: + return + + wo_doc = frappe.get_cached_doc("Work Order", work_order) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.from_bom = 1 + stock_entry.is_return = 1 + stock_entry.work_order = work_order + stock_entry.purpose = "Material Transfer for Manufacture" + stock_entry.bom_no = wo_doc.bom_no + stock_entry.add_transfered_raw_materials_in_items() + stock_entry.set_stock_entry_type() + + return stock_entry diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 3acf5727d1..f354d45381 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -20,6 +20,7 @@ "column_break_11", "transferred_qty", "consumed_qty", + "returned_qty", "available_qty_at_source_warehouse", "available_qty_at_wip_warehouse" ], @@ -97,6 +98,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "depends_on": "eval:!parent.skip_transfer", "fieldname": "consumed_qty", "fieldtype": "Float", @@ -127,11 +129,19 @@ "fieldtype": "Currency", "label": "Amount", "read_only": 1 + }, + { + "columns": 1, + "fieldname": "returned_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Returned Qty ", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-04-13 18:46:32.966416", + "modified": "2022-09-28 10:50:43.512562", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", @@ -140,5 +150,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js index b2428e85b7..2fb4ec6791 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -50,7 +50,7 @@ frappe.query_reports["Work Order Consumed Materials"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["In Process", "Completed", "Stopped"] + options: ["", "In Process", "Completed", "Stopped"] }, { label: __("Excess Materials Consumed"), diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index 8158bc9a02..14e97d3dd7 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -1,6 +1,8 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ @@ -18,7 +20,11 @@ def get_data(report_filters): filters = get_filter_condition(report_filters) wo_items = {} - for d in frappe.get_all("Work Order", filters=filters, fields=fields): + + work_orders = frappe.get_all("Work Order", filters=filters, fields=fields) + returned_materials = get_returned_materials(work_orders) + + for d in work_orders: d.extra_consumed_qty = 0.0 if d.consumed_qty and d.consumed_qty > d.required_qty: d.extra_consumed_qty = d.consumed_qty - d.required_qty @@ -39,6 +45,28 @@ def get_data(report_filters): return data +def get_returned_materials(work_orders): + raw_materials_qty = defaultdict(float) + + raw_materials = frappe.get_all( + "Stock Entry", + fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], + filters=[ + ["Stock Entry", "is_return", "=", 1], + ["Stock Entry Detail", "docstatus", "=", 1], + ["Stock Entry", "work_order", "in", [d.name for d in work_orders]], + ], + ) + + for d in raw_materials: + raw_materials_qty[d.item_code] += d.qty + + for row in work_orders: + row.returned_qty = 0.0 + if raw_materials_qty.get(row.raw_material_item_code): + row.returned_qty = raw_materials_qty.get(row.raw_material_item_code) + + def get_fields(): return [ "`tabWork Order Item`.`parent`", @@ -65,7 +93,7 @@ def get_filter_condition(report_filters): for field in ["name", "production_item", "company", "status"]: value = report_filters.get(field) if value: - key = f"`{field}`" + key = f"{field}" filters.update({key: value}) return filters @@ -112,4 +140,10 @@ def get_columns(): "fieldtype": "Float", "width": 100, }, + { + "label": _("Returned Qty"), + "fieldname": "returned_qty", + "fieldtype": "Float", + "width": 100, + }, ] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 379b7fea90..6a8c21f654 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -316,3 +316,4 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v13_0.update_schedule_type_in_loans +erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index a1d40b739e..0bd3fcdec4 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 0000000000..09e20a9d79 --- /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/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py index 062d24b78b..fd2a2a39cc 100644 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py @@ -3,6 +3,29 @@ from frappe import qb from frappe.utils import create_batch +def remove_duplicate_entries(pl_entries): + unique_vouchers = set() + for x in pl_entries: + unique_vouchers.add( + (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks) + ) + + entries = [] + for x in unique_vouchers: + entries.append( + frappe._dict( + company=x[0], + account=x[1], + party_type=x[2], + party=x[3], + voucher_type=x[4], + voucher_no=x[5], + gle_remarks=x[6], + ) + ) + return entries + + def execute(): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): @@ -34,6 +57,8 @@ def execute(): .run(as_dict=True) ) + pl_entries = remove_duplicate_entries(pl_entries) + if pl_entries: # split into multiple batches, update and commit for each batch batch_size = 1000 diff --git a/erpnext/public/scss/order-page.scss b/erpnext/public/scss/order-page.scss new file mode 100644 index 0000000000..6f5fe5d4d7 --- /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 9ea8416034..b5e97f1c34 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/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index da7576e08d..24375d8252 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -660,7 +660,7 @@ erpnext.PointOfSale.Controller = class { } else { return; } - } else if (available_qty < qty_needed) { + } else if (is_stock_item && available_qty < qty_needed) { frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' @@ -694,7 +694,7 @@ erpnext.PointOfSale.Controller = class { callback(res) { if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; - me.item_stock_map[item_code][warehouse] = res.message[0]; + me.item_stock_map[item_code][warehouse] = res.message; } }); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index b75ffb235e..f9b5bb2e45 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class { if (this.value) { me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); - const available_qty = me.item_stock_map[me.item_row.item_code] && me.item_stock_map[me.item_row.item_code][this.value]; + const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0]; + const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]); if (available_qty === undefined) { me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { // item stock map is updated now reset warehouse me.warehouse_control.set_value(this.value); }) - } else if (available_qty === 0) { + } else if (available_qty === 0 && is_stock_item) { me.warehouse_control.set_value(''); const bold_item_code = me.item_row.item_code.bold(); const bold_warehouse = this.value.bold(); diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 548df318fa..c28f45aed4 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -37,8 +37,10 @@ class Bin(Document): self.set_projected_qty() - self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) - self.db_set("projected_qty", self.projected_qty) + self.db_set( + "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True + ) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): # reserved qty @@ -118,9 +120,9 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def on_doctype_update(): @@ -193,4 +195,5 @@ def update_qty(bin_name, args): "planned_qty": planned_qty, "projected_qty": projected_qty, }, + update_modified=True, ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6bcab737b3..1b9f16814c 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 7e1476d240..e61f0f514e 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 143fe408c3..c8bb1b960e 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 1cee553be5..e35c8bf335 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 adddb41382..9c1c7e5679 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/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b77c3a5134..62697244ba 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1241,6 +1241,37 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(query[0].value, 0) + def test_batch_expiry_for_purchase_receipt(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + 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, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 c4705246b3..d6f9bae5da 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -128,6 +128,9 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return + # This is to avoid TooManyWritesError in case of large reposts + frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index abe98e2933..7e9420d503 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -148,19 +148,19 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "purchase_order", - "fieldtype": "Link", - "label": "Purchase Order", - "options": "Purchase Order" + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order" }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "subcontracting_order", - "fieldtype": "Link", - "label": "Subcontracting Order", - "options": "Subcontracting Order" - }, + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "options": "Subcontracting Order" + }, { "depends_on": "eval:doc.purpose==\"Sales Return\"", "fieldname": "delivery_note_no", @@ -616,6 +616,7 @@ "fieldname": "is_return", "fieldtype": "Check", "hidden": 1, + "in_list_view": 1, "label": "Is Return", "no_copy": 1, "print_hide": 1, @@ -627,7 +628,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-02 05:21:39.060501", + "modified": "2022-10-07 14:39:51.943770", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 738ac330e3..b1167351c4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1073,8 +1073,8 @@ class StockEntry(StockController): # No work order could mean independent Manufacture entry, if so skip validation if self.work_order and self.fg_completed_qty > allowed_qty: frappe.throw( - _("For quantity {0} should not be greater than work order quantity {1}").format( - flt(self.fg_completed_qty), wo_qty + _("For quantity {0} should not be greater than allowed quantity {1}").format( + flt(self.fg_completed_qty), allowed_qty ) ) @@ -1212,13 +1212,19 @@ class StockEntry(StockController): def update_work_order(self): def _validate_work_order(pro_doc): + msg, title = "", "" if flt(pro_doc.docstatus) != 1: - frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) + msg = f"Work Order {self.work_order} must be submitted" if pro_doc.status == "Stopped": - frappe.throw( - _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) - ) + msg = f"Transaction not allowed against stopped Work Order {self.work_order}" + + if self.is_return and pro_doc.status not in ["Completed", "Closed"]: + title = _("Stock Return") + msg = f"Work Order {self.work_order} must be completed or closed" + + if msg: + frappe.throw(_(msg), title=title) if self.job_card: job_doc = frappe.get_doc("Job Card", self.job_card) @@ -1754,10 +1760,12 @@ class StockEntry(StockController): for key, row in available_materials.items(): remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty) - if remaining_qty_to_produce <= 0: + if remaining_qty_to_produce <= 0 and not self.is_return: continue - qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce + qty = flt(row.qty) + if not self.is_return: + qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce item = row.item_details if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): @@ -1781,6 +1789,9 @@ class StockEntry(StockController): self.update_item_in_stock_entry_detail(row, item, qty) def update_item_in_stock_entry_detail(self, row, item, qty) -> None: + if not qty: + return + ste_item_details = { "from_warehouse": item.warehouse, "to_warehouse": "", @@ -1794,6 +1805,9 @@ class StockEntry(StockController): "original_item": item.original_item, } + if self.is_return: + ste_item_details["to_warehouse"] = item.s_warehouse + if row.serial_nos: serial_nos = row.serial_nos if item.batch_no: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js index cbc3491eba..4eb0da11d2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js @@ -1,8 +1,13 @@ frappe.listview_settings['Stock Entry'] = { add_fields: ["`tabStock Entry`.`from_warehouse`", "`tabStock Entry`.`to_warehouse`", - "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`"], + "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`", + "`tabStock Entry`.`is_return`"], get_indicator: function (doc) { - if (doc.docstatus === 0) { + debugger + if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") { + return [__("Material Returned from WIP"), "orange", + "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"]; + } else if (doc.docstatus === 0) { return [__("Draft"), "red", "docstatus,=,0"]; } else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred < 100) { 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 329cd7da09..c64370dcdf 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,6 +153,11 @@ 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) 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") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 50309647de..9ca40c3675 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1053,7 +1053,7 @@ class update_entries_after(object): updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value("Bin", bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html index c7f0d06dff..0072dc280c 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">
Some functionality is disabled for the demo and the data will be cleared regularly.
- -+ {{ _("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 }}
Select the date and your timezone
+{{ _("Select the date and your timezone") }}
Selected date is at
+ {{ _("Selected date is") }} {{ _("at") }}
{{ _("Add details") }}
+