diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 8fc22dd7650..f8eeba84662 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -49,6 +49,7 @@ class AccountingDimension(Document): "Accounting Dimension Detail", "Company", "Account", + "Finance Book", ): msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c77a201ab51..66214cc7485 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -129,7 +129,7 @@ class GLEntry(Document): if not self.get(k): frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) - if not (self.party_type and self.party): + if not self.is_cancelled and not (self.party_type and self.party): account_type = frappe.get_cached_value("Account", self.account, "account_type") if account_type == "Receivable": frappe.throw( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4a45007d50a..55fab670fd8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -812,27 +812,41 @@ frappe.ui.form.on("Payment Entry", { paid_amount: function (frm) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + if (!frm.doc.received_amount) { + if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { + frm.set_value("received_amount", frm.doc.paid_amount); + } else if (company_currency == frm.doc.paid_to_account_currency) { + frm.set_value("received_amount", frm.doc.base_paid_amount); + frm.set_value("base_received_amount", frm.doc.base_paid_amount); + } + } frm.trigger("reset_received_amount"); frm.events.hide_unhide_fields(frm); }, received_amount: function (frm) { + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frm.set_paid_amount_based_on_received_amount = true; - if (!frm.doc.paid_amount && frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { - frm.set_value("paid_amount", frm.doc.received_amount); - - if (frm.doc.target_exchange_rate) { - frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); - } - frm.set_value("base_paid_amount", frm.doc.base_received_amount); - } - frm.set_value( "base_received_amount", flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate) ); + if (!frm.doc.paid_amount) { + if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { + frm.set_value("paid_amount", frm.doc.received_amount); + if (frm.doc.target_exchange_rate) { + frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); + } + frm.set_value("base_paid_amount", frm.doc.base_received_amount); + } else if (company_currency == frm.doc.paid_from_account_currency) { + frm.set_value("paid_amount", frm.doc.base_received_amount); + frm.set_value("base_paid_amount", frm.doc.base_received_amount); + } + } + if (frm.doc.payment_type == "Pay") frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true); else frm.events.set_unallocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 5f191e4800a..3a0a4a8d065 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -224,6 +224,7 @@ "label": "Accounts" }, { + "allow_on_submit": 1, "depends_on": "party", "fieldname": "party_balance", "fieldtype": "Currency", @@ -253,6 +254,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "depends_on": "paid_from", "fieldname": "paid_from_account_balance", "fieldtype": "Currency", @@ -286,6 +288,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "depends_on": "paid_to", "fieldname": "paid_to_account_balance", "fieldtype": "Currency", @@ -806,7 +809,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2025-01-13 16:03:47.169699", + "modified": "2025-01-31 17:27:28.555246", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a63d8d87807..967b12599bf 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -25,6 +25,10 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( get_party_account_based_on_invoice_discounting, ) from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, +) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) @@ -114,6 +118,23 @@ class PaymentEntry(AccountsController): self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() + def validate_for_repost(self): + validate_docs_for_voucher_types(["Payment Entry"]) + validate_docs_for_deferred_accounting([self.name], []) + + def on_update_after_submit(self): + # Flag will be set on Reconciliation + # Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost. + if self.flags.get("ignore_reposting_on_reconciliation"): + return + + self.needs_repost = self.check_if_fields_updated( + fields_to_check=[], child_tables={"references": [], "taxes": [], "deductions": []} + ) + if self.needs_repost: + self.validate_for_repost() + self.repost_accounting_entries() + def set_liability_account(self): # Auto setting liability account should only be done during 'draft' status if self.docstatus > 0 or self.payment_type == "Internal Transfer": diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4c6d9a85aa1..4183cc21ab8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -299,7 +299,8 @@ "oldfieldname": "project_name", "oldfieldtype": "Link", "options": "Project", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "default": "0", @@ -2186,7 +2187,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2025-01-14 11:38:30.446370", + "modified": "2025-02-06 15:59:54.636202", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", 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 3215b93a496..06549973242 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -270,7 +270,10 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): vouchers, voucher_wise_amount = get_invoice_vouchers( - parties, tax_details, inv.company, party_type=party_type + parties, + tax_details, + inv.company, + party_type=party_type, ) payment_entry_vouchers = get_payment_entry_vouchers( @@ -360,11 +363,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): voucher_wise_amount = [] vouchers = [] + ldcs = frappe.db.get_all( + "Lower Deduction Certificate", + filters={ + "valid_from": [">=", tax_details.from_date], + "valid_upto": ["<=", tax_details.to_date], + "company": company, + "supplier": ["in", parties], + }, + fields=["supplier", "valid_from", "valid_upto", "rate"], + ) + doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" field = [ "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total", "name", "grand_total", + "posting_date", ] filters = { @@ -383,18 +398,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): invoices_details = frappe.get_all(doctype, filters=filters, fields=field) for d in invoices_details: - vouchers.append(d.name) - voucher_wise_amount.append( - frappe._dict( - { - "voucher_name": d.name, - "voucher_type": doctype, - "taxable_amount": d.base_net_total, - "grand_total": d.grand_total, - } - ) + d = frappe._dict( + { + "voucher_name": d.name, + "voucher_type": doctype, + "taxable_amount": d.base_net_total, + "grand_total": d.grand_total, + "posting_date": d.posting_date, + } ) + if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]: + if ldc[0].supplier in parties and ldc[0].rate == 0: + d.update({"taxable_amount": 0}) + + vouchers.append(d.voucher_name) + voucher_wise_amount.append(d) + journal_entries_details = frappe.db.sql( """ SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index c4ab2f94581..6ab6bcc08a2 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, today +from frappe.utils import add_days, add_months, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year @@ -666,6 +666,49 @@ class TestTaxWithholdingCategory(FrappeTestCase): pi2.cancel() pi3.cancel() + def test_ldc_at_0_rate(self): + frappe.db.set_value( + "Supplier", + "Test LDC Supplier", + { + "tax_withholding_category": "Test Service Category", + "pan": "ABCTY1234D", + }, + ) + + fiscal_year = get_fiscal_year(today(), company="_Test Company") + valid_from = fiscal_year[1] + valid_upto = add_months(valid_from, 1) + create_lower_deduction_certificate( + supplier="Test LDC Supplier", + certificate_no="1AE0423AAJ", + tax_withholding_category="Test Service Category", + tax_rate=0, + limit=50000, + valid_from=valid_from, + valid_upto=valid_upto, + ) + + pi1 = create_purchase_invoice( + supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True + ) + pi1.submit() + self.assertEqual(pi1.taxes, []) + + pi2 = create_purchase_invoice( + supplier="Test LDC Supplier", + rate=35000, + posting_date=add_days(valid_upto, 1), + set_posting_time=True, + ) + pi2.submit() + self.assertEqual(len(pi2.taxes), 1) + # pi1 net total shouldn't be included as it lies within LDC at rate of '0' + self.assertEqual(pi2.taxes[0].tax_amount, 3500) + + pi1.cancel() + pi2.cancel() + def set_previous_fy_and_tax_category(self): test_company = "_Test Company" category = "Cumulative Threshold TDS" @@ -823,7 +866,8 @@ def create_purchase_invoice(**args): pi = frappe.get_doc( { "doctype": "Purchase Invoice", - "posting_date": today(), + "set_posting_time": args.set_posting_time or False, + "posting_date": args.posting_date or today(), "apply_tds": 0 if args.do_not_apply_tds else 1, "supplier": args.supplier, "company": "_Test Company", @@ -1161,7 +1205,9 @@ def create_tax_withholding_category( ).insert() -def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit): +def create_lower_deduction_certificate( + supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None +): fiscal_year = get_fiscal_year(today(), company="_Test Company") if not frappe.db.exists("Lower Deduction Certificate", certificate_no): frappe.get_doc( @@ -1172,8 +1218,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r "certificate_no": certificate_no, "tax_withholding_category": tax_withholding_category, "fiscal_year": fiscal_year[0], - "valid_from": fiscal_year[1], - "valid_upto": fiscal_year[2], + "valid_from": valid_from or fiscal_year[1], + "valid_upto": valid_upto or fiscal_year[2], "rate": tax_rate, "certificate_limit": limit, } diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e5e43aefa32..7d0bf2cca11 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -680,11 +680,15 @@ def make_reverse_gl_entries( debit_in_account_currency = new_gle.get("debit_in_account_currency", 0) credit_in_account_currency = new_gle.get("credit_in_account_currency", 0) + debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0) + credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0) new_gle["debit"] = credit new_gle["credit"] = debit new_gle["debit_in_account_currency"] = credit_in_account_currency new_gle["credit_in_account_currency"] = debit_in_account_currency + new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency + new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"] new_gle["is_cancelled"] = 1 diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 4802b0f35c1..fe17924d527 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -219,15 +219,34 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for src in gross_profit_data.grouped_data: - row = [] - for col in group_wise_columns.get(scrub(filters.group_by)): - row.append(src.get(col)) + total_base_amount = 0 + total_buying_amount = 0 - row.append(filters.currency) + group_columns = group_wise_columns.get(scrub(filters.group_by)) + + for src in gross_profit_data.grouped_data: + total_base_amount += src.base_amount or 0.00 + total_buying_amount += src.buying_amount or 0.00 + + row = [src.get(col) for col in group_columns] + [filters.currency] data.append(row) + total_gross_profit = total_base_amount - total_buying_amount + currency_precision = cint(frappe.db.get_default("currency_precision")) or 3 + gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0 + + total_row = { + group_columns[0]: "Total", + "base_amount": total_base_amount, + "buying_amount": total_buying_amount, + "gross_profit": total_gross_profit, + "gross_profit_percent": flt(gross_profit_percent, currency_precision), + } + + total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]] + data.append(total_row) + def get_columns(group_wise_columns, filters): columns = [] diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b3c82e84192..db7a3a2a70f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -773,6 +773,8 @@ def update_reference_in_payment_entry( frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict ) + # Ledgers will be reposted by Reconciliation tool + payment_entry.flags.ignore_reposting_on_reconciliation = True if not do_not_save: payment_entry.save(ignore_permissions=True) return row, update_advance_paid diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 3a71733a003..bef41394742 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -394,7 +394,11 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier= }, "Request for Quotation Item": { "doctype": "Supplier Quotation Item", - "field_map": {"name": "request_for_quotation_item", "parent": "request_for_quotation"}, + "field_map": { + "name": "request_for_quotation_item", + "parent": "request_for_quotation", + "project_name": "project", + }, }, }, target_doc, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 715a73e0da6..e75fdc126e6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -821,7 +821,7 @@ class AccountsController(TransactionBase): and item.get("use_serial_batch_fields") ) ): - if fieldname == "batch_no" and not item.batch_no: + if fieldname == "batch_no" and not item.batch_no and not item.is_free_item: item.set("rate", ret.get("rate")) item.set("price_list_rate", ret.get("price_list_rate")) item.set(fieldname, value) @@ -1903,22 +1903,22 @@ class AccountsController(TransactionBase): continue ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item)) + based_on_amt = flt(item.get(based_on)) if not ref_amt: - frappe.msgprint( - _("System will not check over billing since amount for Item {0} in {1} is zero").format( - item.item_code, ref_dt - ), - title=_("Warning"), - indicator="orange", - ) + if based_on_amt: # Skip warning for free items + frappe.msgprint( + _( + "System will not check over billing since amount for Item {0} in {1} is zero" + ).format(item.item_code, ref_dt), + title=_("Warning"), + indicator="orange", + ) continue already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on) - total_billed_amt = flt( - flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item) - ) + total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item)) allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for( item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount" diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e6e7d7612c5..721af21be99 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1620,7 +1620,9 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create "project": work_order.project, "company": work_order.company, "sequence_id": row.get("sequence_id"), - "wip_warehouse": work_order.wip_warehouse, + "wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse") + if not work_order.skip_transfer or work_order.from_wip_warehouse + else work_order.source_warehouse or row.get("source_warehouse"), "hour_rate": row.get("hour_rate"), "serial_no": row.get("serial_no"), } diff --git a/erpnext/patches/v14_0/update_reports_with_range.py b/erpnext/patches/v14_0/update_reports_with_range.py index 014fba883fc..ccfa4936cfd 100644 --- a/erpnext/patches/v14_0/update_reports_with_range.py +++ b/erpnext/patches/v14_0/update_reports_with_range.py @@ -27,7 +27,7 @@ def update_reference_reports(reference_report): def update_report_json(report): - report_json = json.loads(report.json) + report_json = json.loads(report.json) if report.get("json") else {} report_filter = report_json.get("filters") if not report_filter: diff --git a/erpnext/patches/v15_0/sync_auto_reconcile_config.py b/erpnext/patches/v15_0/sync_auto_reconcile_config.py index 721364dcaa6..be92ad99536 100644 --- a/erpnext/patches/v15_0/sync_auto_reconcile_config.py +++ b/erpnext/patches/v15_0/sync_auto_reconcile_config.py @@ -11,16 +11,17 @@ def execute(): frappe.db.set_single_value("Accounts Settings", "reconciliation_queue_size", 5) # Create Scheduler Event record if it doesn't exist - method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs" - if not frappe.db.get_all( - "Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method} - ): - frappe.get_doc( - { - "doctype": "Scheduler Event", - "scheduled_against": "Process Payment Reconciliation", - "method": method, - } - ).save() + if frappe.reload_doc("core", "doctype", "scheduler_event"): + method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs" + if not frappe.db.get_all( + "Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method} + ): + frappe.get_doc( + { + "doctype": "Scheduler Event", + "scheduled_against": "Process Payment Reconciliation", + "method": method, + } + ).save() - sync_auto_reconcile_config(15) + sync_auto_reconcile_config(15) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5253cd0eae5..4ed8ffc0077 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -86,8 +86,6 @@ class Project(Document): ), ) - self.update_costing() - def before_print(self, settings=None): self.onload() diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index 1e0a8805075..43ff59a1006 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -331,22 +331,19 @@ def sales_invoice_on_submit(doc, method): ]: return - if not len(doc.payment_schedule): - frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing")) - else: - for schedule in doc.payment_schedule: - if not schedule.mode_of_payment: - frappe.throw( - _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), - title=_("E-Invoicing Information Missing"), - ) - elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): - frappe.throw( - _("Row {0}: Please set the correct code on Mode of Payment {1}").format( - schedule.idx, schedule.mode_of_payment - ), - title=_("E-Invoicing Information Missing"), - ) + for schedule in doc.payment_schedule: + if not schedule.mode_of_payment: + frappe.throw( + _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), + title=_("E-Invoicing Information Missing"), + ) + elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): + frappe.throw( + _("Row {0}: Please set the correct code on Mode of Payment {1}").format( + schedule.idx, schedule.mode_of_payment + ), + title=_("E-Invoicing Information Missing"), + ) prepare_and_attach_invoice(doc) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 1525b9632de..4db7bf6f003 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1151,7 +1151,8 @@ "label": "Project", "oldfieldname": "project", "oldfieldtype": "Link", - "options": "Project" + "options": "Project", + "search_index": 1 }, { "fieldname": "party_account_currency", @@ -1654,7 +1655,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-11-26 12:42:06.872527", + "modified": "2025-02-06 16:02:20.320877", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1732,4 +1733,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 9de6dbbd429..dee052912ff 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -748,6 +748,7 @@ erpnext.PointOfSale.ItemCart = class { frappe.utils.play_sound("error"); return; } + this.highlight_numpad_btn($btn, current_action); if (first_click_event || field_to_edit_changed) { this.prev_action = current_action; @@ -793,7 +794,6 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = current_action; } - this.highlight_numpad_btn($btn, current_action); this.events.numpad_event(this.numpad_value, this.prev_action); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 0adbf2280dc..33dd0489ba2 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -41,6 +41,7 @@ erpnext.PointOfSale.Payment = class { } make_invoice_fields_control() { + this.reqd_invoice_fields = []; frappe.db.get_doc("POS Settings", undefined).then((doc) => { const fields = doc.invoice_fields; if (!fields.length) return; @@ -67,6 +68,9 @@ erpnext.PointOfSale.Payment = class { }, }; } + if (df.reqd && (df.fieldtype !== "Button" || !df.read_only)) { + this.reqd_invoice_fields.push({ fieldname: df.fieldname, label: df.label }); + } this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ df: { @@ -204,7 +208,11 @@ erpnext.PointOfSale.Payment = class { const paid_amount = doc.paid_amount; const items = doc.items; - if (paid_amount == 0 || !items.length) { + if (!this.validate_reqd_invoice_fields()) { + return; + } + + if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) { const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order."); @@ -620,4 +628,20 @@ erpnext.PointOfSale.Payment = class { .replace(/^[^_a-zA-Z\p{L}]+/u, "") .toLowerCase(); } + + validate_reqd_invoice_fields() { + const doc = this.events.get_frm().doc; + let validation_flag = true; + for (let field of this.reqd_invoice_fields) { + if (!doc[field.fieldname]) { + validation_flag = false; + frappe.show_alert({ + message: __("{0} is a mandatory field.", [field.label]), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + } + return validation_flag; + } }; diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 8948211b2e5..22c1ce7927a 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -182,8 +182,6 @@ "read_only": 1 }, { - "fetch_from": "user_id.user_image", - "fetch_if_empty": 1, "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, @@ -824,7 +822,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2024-01-03 17:36:20.984421", + "modified": "2025-02-07 13:54:40.122345", "modified_by": "Administrator", "module": "Setup", "name": "Employee", @@ -873,4 +871,4 @@ "states": [], "title_field": "employee_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 31568fe50dc..ad062cd6473 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -64,14 +64,12 @@ class Employee(NestedSet): def validate_user_details(self): if self.user_id: - data = frappe.db.get_value("User", self.user_id, ["enabled", "user_image"], as_dict=1) + data = frappe.db.get_value("User", self.user_id, ["enabled"], as_dict=1) if not data: self.user_id = None return - if data.get("user_image") and self.image == "": - self.image = data.get("user_image") self.validate_for_enabled_user_id(data.get("enabled", 0)) self.validate_duplicate_user_id() diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index f59b60a3f51..69572e661f8 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -482,7 +482,7 @@ def make_request_for_quotation(source_name, target_doc=None): "field_map": [ ["name", "material_request_item"], ["parent", "material_request"], - ["uom", "uom"], + ["project", "project_name"], ], }, }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 44e0145ea6c..0328c447ec2 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1360,26 +1360,25 @@ def get_item_account_wise_additional_cost(purchase_document): for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: + exchange_rate = account.exchange_rate or 1 item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault( account.expense_account, {"amount": 0.0, "base_amount": 0.0} ) - if total_item_cost > 0: - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ - account.expense_account - ]["amount"] += account.amount * item.get(based_on_field) / total_item_cost + item_row = item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ] - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ - account.expense_account - ]["base_amount"] += account.base_amount * item.get(based_on_field) / total_item_cost + if total_item_cost > 0: + item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost + + item_row["base_amount"] += ( + account.base_amount * item.get(based_on_field) / total_item_cost + ) else: - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ - account.expense_account - ]["amount"] += item.applicable_charges - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ - account.expense_account - ]["base_amount"] += item.applicable_charges + item_row["amount"] += item.applicable_charges / exchange_rate + item_row["base_amount"] += item.applicable_charges return item_account_wise_cost diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 59ef43e31a8..3ff76ee818b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -1,13 +1,12 @@ { "actions": [], - "autoname": "naming_series:", + "autoname": "hash", "creation": "2023-08-11 17:22:12.907518", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "item_details_tab", - "naming_series", "company", "item_name", "has_serial_no", @@ -152,6 +151,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "avg_rate", "fieldtype": "Float", "label": "Avg Rate", @@ -159,6 +159,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "total_amount", "fieldtype": "Float", "label": "Total Amount", @@ -166,6 +167,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Qty", @@ -195,12 +197,6 @@ "reqd": 1, "search_index": 1 }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "SABB-.########" - }, { "default": "0", "depends_on": "eval:doc.voucher_type == \"Purchase Receipt\"", @@ -251,11 +247,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-15 15:22:24.003486", + "modified": "2025-02-12 10:53:32.090309", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", - "naming_rule": "By \"Naming Series\" field", + "naming_rule": "Random", "owner": "Administrator", "permissions": [ { @@ -389,4 +385,4 @@ "sort_order": "DESC", "states": [], "title_field": "item_code" -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index dcd876deeb7..c37d2464ff0 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -55,9 +55,7 @@ class SerialandBatchBundle(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import ( - SerialandBatchEntry, - ) + from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import SerialandBatchEntry amended_from: DF.Link | None avg_rate: DF.Float @@ -70,7 +68,6 @@ class SerialandBatchBundle(Document): item_code: DF.Link item_group: DF.Link | None item_name: DF.Data | None - naming_series: DF.Literal["SABB-.########"] posting_date: DF.Date | None posting_time: DF.Time | None returned_against: DF.Data | None diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 85acc762969..9307eee46f1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -295,6 +295,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { qty: function (frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); + + let row = locals[cdt][cdn]; + if (row.use_serial_batch_fields && !row.qty && row.serial_no) { + frappe.model.set_value(cdt, cdn, "serial_no", ""); + } }, valuation_rate: function (frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 85c74480e7d..a9f99e177fd 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1372,13 +1372,13 @@ def get_stock_balance_for( or 0 ) - if row.use_serial_batch_fields and row.batch_no: + if row.use_serial_batch_fields and row.batch_no and (qty or row.current_qty): rate = get_incoming_rate( frappe._dict( { "item_code": row.item_code, "warehouse": row.warehouse, - "qty": row.qty * -1, + "qty": flt(qty or row.current_qty) * -1, "batch_no": row.batch_no, "company": company, "posting_date": posting_date, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 48a27a25962..77d2f7eaebb 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1408,6 +1408,44 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertTrue(sr.items[0].current_serial_and_batch_bundle) self.assertFalse(sr.items[0].serial_and_batch_bundle) + def test_stock_reco_batch_item_current_valuation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + # Add new serial nos + item_code = "Stock-Reco-batch-Item-1234" + warehouse = "_Test Warehouse - _TC" + self.make_item( + item_code, + frappe._dict( + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "JJ-SRI1234-.#####", + } + ), + ) + + se = make_stock_entry( + item_code=item_code, + target=warehouse, + qty=1, + basic_rate=100, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=0, rate=100, do_not_save=1 + ) + + sr.items[0].batch_no = batch_no + sr.items[0].use_serial_batch_fields = 1 + sr.save() + self.assertEqual(sr.items[0].current_valuation_rate, 100) + self.assertEqual(sr.difference_amount, 100 * -1) + self.assertTrue(sr.items[0].qty == 0) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 291aa370056..5c5fe5db276 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -187,6 +187,9 @@ def update_stock(ctx, out, doc=None): and out.warehouse and out.stock_qty > 0 ): + if doc and isinstance(doc, dict): + doc = frappe._dict(doc) + kwargs = frappe._dict( { "item_code": ctx.item_code, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 650c66dd408..993d918f8bc 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1080,6 +1080,7 @@ class SerialBatchCreation: def set_serial_batch_entries(self, doc): incoming_rate = self.get("incoming_rate") + precision = frappe.get_precision("Serial and Batch Entry", "qty") if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) if self.has_batch_no: @@ -1109,7 +1110,8 @@ class SerialBatchCreation: "entries", { "batch_no": batch_no, - "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1), + "qty": flt(batch_qty, precision) + * (-1 if self.type_of_transaction == "Outward" else 1), "incoming_rate": incoming_rate, }, ) diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index ade66dd481f..315478fc649 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -40,7 +40,7 @@
@@ -72,8 +72,7 @@