diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 31991648158..e9b383776f3 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -73,7 +73,9 @@ "remarks_section", "general_ledger_remarks_length", "column_break_lvjk", - "receivable_payable_remarks_length" + "receivable_payable_remarks_length", + "payment_request_settings", + "create_pr_in_draft_status" ], "fields": [ { @@ -475,6 +477,18 @@ "fieldname": "calculate_depr_using_total_days", "fieldtype": "Check", "label": "Calculate daily depreciation using total days in depreciation period" + }, + { + "description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.", + "fieldname": "payment_request_settings", + "fieldtype": "Tab Break", + "label": "Payment Request" + }, + { + "default": "1", + "fieldname": "create_pr_in_draft_status", + "fieldtype": "Check", + "label": "Create in Draft Status" } ], "icon": "icon-cog", @@ -482,7 +496,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-12 00:24:20.957726", + "modified": "2024-07-26 06:48:52.714630", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 93ff1e207c9..608b3a96f2f 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -35,6 +35,7 @@ class AccountsSettings(Document): book_tax_discount_loss: DF.Check calculate_depr_using_total_days: DF.Check check_supplier_invoice_uniqueness: DF.Check + create_pr_in_draft_status: DF.Check credit_controller: DF.Link | None delete_linked_ledger_entries: DF.Check determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 2c831cf7c57..d290d794df1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -25,30 +25,6 @@ frappe.ui.form.on("Journal Entry", { refresh: function (frm) { erpnext.toggle_naming_series(); - if (frm.doc.repost_required && frm.doc.docstatus === 1) { - frm.set_intro( - __( - "Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update." - ) - ); - frm.add_custom_button(__("Repost Accounting Entries"), () => { - frm.call({ - doc: frm.doc, - method: "repost_accounting_entries", - freeze: true, - freeze_message: __("Reposting..."), - callback: (r) => { - if (!r.exc) { - frappe.msgprint(__("Accounting Entries are reposted.")); - frm.refresh(); - } - }, - }); - }) - .removeClass("btn-default") - .addClass("btn-warning"); - } - if (frm.doc.docstatus > 0) { frm.add_custom_button( __("Ledger"), diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 91febb34052..4184bdaabb9 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -64,8 +64,7 @@ "stock_entry", "subscription_section", "auto_repeat", - "amended_from", - "repost_required" + "amended_from" ], "fields": [ { @@ -544,15 +543,6 @@ "label": "Is System Generated", "no_copy": 1, "read_only": 1 - }, - { - "default": "0", - "fieldname": "repost_required", - "fieldtype": "Check", - "hidden": 1, - "label": "Repost Required", - "print_hide": 1, - "read_only": 1 } ], "icon": "fa fa-file-text", @@ -567,7 +557,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2023-11-23 12:11:04.128015", + "modified": "2024-07-18 15:32:29.413598", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -618,4 +608,4 @@ "states": [], "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 12b9800e688..6adc8be3f7d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -47,9 +47,7 @@ class JournalEntry(AccountsController): if TYPE_CHECKING: from frappe.types import DF - from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import ( - JournalEntryAccount, - ) + from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount accounts: DF.Table[JournalEntryAccount] amended_from: DF.Link | None @@ -197,14 +195,10 @@ class JournalEntry(AccountsController): self.update_booked_depreciation() def on_update_after_submit(self): - if hasattr(self, "repost_required"): - self.needs_repost = self.check_if_fields_updated( - fields_to_check=[], child_tables={"accounts": []} - ) - if self.needs_repost: - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) - self.repost_accounting_entries() + self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []}) + if self.needs_repost: + self.validate_for_repost() + self.repost_accounting_entries() def on_cancel(self): # References for this Journal are removed on the `on_cancel` event in accounts_controller diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index 4033fc08233..7311ed9f8be 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", { // payment Entry if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") { - frm.add_custom_button(__("Create Payment Entries"), function () { + frm.add_custom_button(__("Create Journal Entries"), function () { frm.trigger("make_payment_records"); }); } diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 03435a26011..48bb6f2ae86 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -500,7 +500,8 @@ def make_payment_request(**args): if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True - pr.insert(ignore_permissions=True) + if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True): + pr.insert(ignore_permissions=True) if args.submit_doc: pr.submit() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index c0a43395216..9c8c8cdfb03 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -77,31 +77,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); } - if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) { - this.frm.set_intro( - __( - "Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update." - ) - ); - this.frm - .add_custom_button(__("Repost Accounting Entries"), () => { - this.frm.call({ - doc: this.frm.doc, - method: "repost_accounting_entries", - freeze: true, - freeze_message: __("Reposting..."), - callback: (r) => { - if (!r.exc) { - frappe.msgprint(__("Accounting Entries are reposted.")); - me.frm.refresh(); - } - }, - }); - }) - .removeClass("btn-default") - .addClass("btn-warning"); - } - if (!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0) { if (doc.on_hold) { this.frm.add_custom_button( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index cf08d08ce4b..3751c027c97 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -170,7 +170,6 @@ "against_expense_account", "column_break_63", "unrealized_profit_loss_account", - "repost_required", "subscription_section", "subscription", "auto_repeat", @@ -364,7 +363,8 @@ "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date" + "label": "Release Date", + "search_index": 1 }, { "fieldname": "cb_17", @@ -1603,15 +1603,6 @@ "fieldtype": "Check", "label": "Use Company Default Round Off Cost Center" }, - { - "default": "0", - "fieldname": "repost_required", - "fieldtype": "Check", - "hidden": 1, - "label": "Repost Required", - "options": "Account", - "read_only": 1 - }, { "default": "0", "fieldname": "use_transaction_date_exchange_rate", @@ -1639,7 +1630,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-04-11 11:28:42.802211", + "modified": "2024-07-25 19:42:36.931278", "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 15f53d996da..c33b2c0ff17 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -159,7 +159,6 @@ class PurchaseInvoice(BuyingController): rejected_warehouse: DF.Link | None release_date: DF.Date | None remarks: DF.SmallText | None - repost_required: DF.Check represents_company: DF.Link | None return_against: DF.Link | None rounded_total: DF.Currency @@ -796,19 +795,17 @@ class PurchaseInvoice(BuyingController): self.process_common_party_accounting() def on_update_after_submit(self): - if hasattr(self, "repost_required"): - fields_to_check = [ - "cash_bank_account", - "write_off_account", - "unrealized_profit_loss_account", - "is_opening", - ] - child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} - self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - if self.needs_repost: - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) - self.repost_accounting_entries() + fields_to_check = [ + "cash_bank_account", + "write_off_account", + "unrealized_profit_loss_account", + "is_opening", + ] + child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + if self.needs_repost: + self.validate_for_repost() + self.repost_accounting_entries() def make_gl_entries(self, gl_entries=None, from_repost=False): update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -1705,6 +1702,9 @@ class PurchaseInvoice(BuyingController): self.db_set("release_date", None) def set_tax_withholding(self): + self.set("advance_tax", []) + self.set("tax_withheld_vouchers", []) + if not self.apply_tds: return @@ -1746,8 +1746,6 @@ class PurchaseInvoice(BuyingController): self.remove(d) ## Add pending vouchers on which tax was withheld - self.set("tax_withheld_vouchers", []) - for voucher_no, voucher_details in voucher_wise_amount.items(): self.append( "tax_withheld_vouchers", @@ -1762,7 +1760,6 @@ class PurchaseInvoice(BuyingController): self.calculate_taxes_and_totals() def allocate_advance_tds(self, tax_withholding_details, advance_taxes): - self.set("advance_tax", []) for tax in advance_taxes: allocated_amount = 0 pending_amount = flt(tax.tax_amount - tax.allocated_amount) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 68e9d2b0e00..eb7c486f2e1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2014,8 +2014,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ["Service - _TC", 1000, 0.0, nowdate()], ] check_gl_entries(self, pi.name, expected_gle, nowdate()) - pi.load_from_db() - self.assertFalse(pi.repost_required) @change_settings("Buying Settings", {"supplier_group": None}) def test_purchase_invoice_without_supplier_group(self): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 797d00fe3ec..87b71cd793f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -68,31 +68,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); - if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) { - this.frm.set_intro( - __( - "Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update." - ) - ); - this.frm - .add_custom_button(__("Repost Accounting Entries"), () => { - this.frm.call({ - doc: this.frm.doc, - method: "repost_accounting_entries", - freeze: true, - freeze_message: __("Reposting..."), - callback: (r) => { - if (!r.exc) { - frappe.msgprint(__("Accounting Entries are reposted")); - me.frm.refresh(); - } - }, - }); - }) - .removeClass("btn-default") - .addClass("btn-warning"); - } - if (this.frm.doc.is_return) { this.frm.return_print_format = "Sales Invoice Return"; } @@ -502,11 +477,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( frappe.msgprint(__("Please specify Company to proceed")); } else { var me = this; + const for_validate = me.frm.doc.is_return ? true : false; return this.frm.call({ doc: me.frm.doc, method: "set_missing_values", args: { - for_validate: true, + for_validate: for_validate, }, callback: function (r) { if (!r.exc) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 90b6e768092..c44afd555e0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -213,7 +213,6 @@ "is_internal_customer", "is_discounted", "remarks", - "repost_required", "connections_tab" ], "fields": [ @@ -2125,15 +2124,6 @@ "label": "Write Off", "width": "50%" }, - { - "default": "0", - "fieldname": "repost_required", - "fieldtype": "Check", - "hidden": 1, - "label": "Repost Required", - "no_copy": 1, - "read_only": 1 - }, { "fieldname": "incoterm", "fieldtype": "Link", @@ -2188,7 +2178,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-06-07 16:49:32.458402", + "modified": "2024-07-18 15:30:39.428519", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0d48cf229e5..24e61b0d221 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -161,7 +161,6 @@ class SalesInvoice(SellingController): project: DF.Link | None redeem_loyalty_points: DF.Check remarks: DF.SmallText | None - repost_required: DF.Check represents_company: DF.Link | None return_against: DF.Link | None rounded_total: DF.Currency @@ -556,7 +555,6 @@ class SalesInvoice(SellingController): self.repost_future_sle_and_gle() self.db_set("status", "Cancelled") - self.db_set("repost_required", 0) if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction": update_company_current_month_sales(self.company) @@ -706,25 +704,23 @@ class SalesInvoice(SellingController): data.sales_invoice = sales_invoice def on_update_after_submit(self): - if hasattr(self, "repost_required"): - fields_to_check = [ - "additional_discount_account", - "cash_bank_account", - "account_for_change_amount", - "write_off_account", - "loyalty_redemption_account", - "unrealized_profit_loss_account", - "is_opening", - ] - child_tables = { - "items": ("income_account", "expense_account", "discount_account"), - "taxes": ("account_head",), - } - self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - if self.needs_repost: - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) - self.repost_accounting_entries() + fields_to_check = [ + "additional_discount_account", + "cash_bank_account", + "account_for_change_amount", + "write_off_account", + "loyalty_redemption_account", + "unrealized_profit_loss_account", + "is_opening", + ] + child_tables = { + "items": ("income_account", "expense_account", "discount_account"), + "taxes": ("account_head",), + } + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + if self.needs_repost: + self.validate_for_repost() + self.repost_accounting_entries() def set_paid_amount(self): paid_amount = 0.0 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ee4e82b4dfa..9197f2e091a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2952,9 +2952,6 @@ class TestSalesInvoice(FrappeTestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - si.load_from_db() - self.assertFalse(si.repost_required) - def test_asset_depreciation_on_sale_with_pro_rata(self): """ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. 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 44096714ca7..8a9d1584f7d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -268,6 +268,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N vouchers, voucher_wise_amount = get_invoice_vouchers( parties, tax_details, inv.company, party_type=party_type ) + + payment_entry_vouchers = get_payment_entry_vouchers( + parties, tax_details, inv.company, party_type=party_type + ) + advance_vouchers = get_advance_vouchers( parties, company=inv.company, @@ -275,7 +280,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N to_date=tax_details.to_date, party_type=party_type, ) - taxable_vouchers = vouchers + advance_vouchers + + taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers tax_deducted_on_advances = 0 if inv.doctype == "Purchase Invoice": @@ -387,6 +393,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): return vouchers, voucher_wise_amount +def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"): + payment_entry_filters = { + "party_type": party_type, + "party": ("in", parties), + "docstatus": 1, + "apply_tax_withholding_amount": 1, + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "tax_withholding_category": tax_details.get("tax_withholding_category"), + "company": company, + } + + return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name") + + def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"): """ Use Payment Ledger to fetch unallocated Advance Payments diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 30775c7fa66..f8511d2f497 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -139,6 +139,7 @@ class ReceivablePayableReport: paid_in_account_currency=0.0, credit_note_in_account_currency=0.0, outstanding_in_account_currency=0.0, + cost_center=ple.cost_center, ) self.get_invoices(ple) @@ -253,7 +254,7 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency - if ple.cost_center: + if not row.cost_center and ple.cost_center: row.cost_center = str(ple.cost_center) def update_sub_total_row(self, row, party): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 5e2adc42d84..c4baa4e4842 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): si = si.submit() return si - def create_payment_entry(self, docname): + def create_payment_entry(self, docname, do_not_submit=False): pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40) pe.paid_from = self.debit_to pe.insert() - pe.submit() + if not do_not_submit: + pe.submit() + return pe def create_credit_note(self, docname, do_not_submit=False): credit_note = create_sales_invoice( @@ -984,3 +986,40 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): expected_data_after_payment, [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], ) + + def test_cost_center_on_report_output(self): + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + # check invoice grand total and invoiced column's value for 3 payment terms + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.cost_center = self.cost_center + si.save().submit() + + new_cc = frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": "East Wing", + "parent_cost_center": self.company + " - " + self.company_abbr, + "company": self.company, + } + ) + new_cc.save() + + # check invoice grand total, invoiced, paid and outstanding column's value after payment + pe = self.create_payment_entry(si.name, do_not_submit=True) + pe.cost_center = new_cc.name + pe.save().submit() + report = execute(filters) + + expected_data_after_payment = [si.name, si.cost_center, 60] + + self.assertEqual(len(report[1]), 1) + row = report[1][0] + self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding]) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 8bc05273092..83f664c984a 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -315,8 +315,9 @@ def apply_conditions(query, pi, pii, filters): def get_items(filters, additional_table_columns): - pi = frappe.qb.DocType("Purchase Invoice") - pii = frappe.qb.DocType("Purchase Invoice Item") + doctype = "Purchase Invoice" + pi = frappe.qb.DocType(doctype) + pii = frappe.qb.DocType(f"{doctype} Item") Item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(pi) @@ -353,6 +354,7 @@ def get_items(filters, additional_table_columns): pi.mode_of_payment, ) .where(pi.docstatus == 1) + .where(pii.parenttype == doctype) ) if filters.get("supplier"): diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index cf08e45c537..604c0a6569d 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -410,8 +410,9 @@ def apply_group_by_conditions(query, si, ii, filters): def get_items(filters, additional_query_columns, additional_conditions=None): - si = frappe.qb.DocType("Sales Invoice") - sii = frappe.qb.DocType("Sales Invoice Item") + doctype = "Sales Invoice" + si = frappe.qb.DocType(doctype) + sii = frappe.qb.DocType(f"{doctype} Item") item = frappe.qb.DocType("Item") query = ( @@ -459,6 +460,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None): sii.qty, ) .where(si.docstatus == 1) + .where(sii.parenttype == doctype) ) if additional_query_columns: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2558e976bea..0450221222d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1598,6 +1598,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None: create_err_and_its_journals(companies) +def auto_create_exchange_rate_revaluation_monthly() -> None: + """ + Executed by background job + """ + companies = frappe.db.get_all( + "Company", + filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"}, + fields=["name", "submit_err_jv"], + ) + create_err_and_its_journals(companies) + + def get_payment_ledger_entries(gl_entries, cancel=0): ple_map = [] if gl_entries: diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 742cc3eaa49..586385adb0d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -740,7 +740,7 @@ class TestDepreciationMethods(AssetSetup): available_for_use_date="2030-06-06", is_existing_asset=1, opening_number_of_booked_depreciations=2, - opening_accumulated_depreciation=47095.89, + opening_accumulated_depreciation=47178.08, expected_value_after_useful_life=10000, depreciation_start_date="2032-12-31", total_number_of_depreciations=3, @@ -748,7 +748,7 @@ class TestDepreciationMethods(AssetSetup): ) self.assertEqual(asset.status, "Draft") - expected_schedules = [["2032-12-31", 42904.11, 90000.0]] + expected_schedules = [["2032-12-31", 30000.0, 77178.08], ["2033-06-06", 12821.92, 90000.0]] schedules = [ [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in get_depr_schedule(asset.name, "Draft") diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index c533a634a5b..02fcb8efb3d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -552,9 +552,18 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False): # if not existing asset, from_date = available_for_use_date # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 # from_date = 01/01/2022 - from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False) - days = date_diff(row.depreciation_start_date, from_date) + 1 - total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) + if row.depreciation_method in ("Straight Line", "Manual"): + prev_depreciation_start_date = add_months( + row.depreciation_start_date, + (row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations, + ) + from_date = asset_doc.available_for_use_date + days = date_diff(prev_depreciation_start_date, from_date) + 1 + total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation) + else: + from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False) + days = date_diff(row.depreciation_start_date, from_date) + 1 + total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) if days <= 0: frappe.throw( _( @@ -682,20 +691,15 @@ def get_straight_line_or_manual_depr_amount( # if the Depreciation Schedule is being prepared for the first time else: if row.daily_prorata_based: - amount = ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) + amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) return get_daily_prorata_based_straight_line_depr( asset, row, schedule_idx, number_of_pending_depreciations, amount ) else: - return ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations) + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) + return depreciation_amount def get_daily_prorata_based_straight_line_depr( @@ -725,7 +729,11 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount): ) ), add_days( - get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), + add_months( + row.depreciation_start_date, + (row.frequency_of_depreciation * (asset.opening_number_of_booked_depreciations + 1)) + * -1, + ), 1, ), ) @@ -904,7 +912,7 @@ def _get_daily_prorata_based_default_wdv_or_dd_depr_amount( def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value): - """ " + """ Returns monthly depreciation amount when year changes 1. Calculate per day depr based on new year 2. Calculate monthly amount based on new per day amount diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py index 107d38057a2..c9fa0ba59da 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py @@ -75,6 +75,116 @@ class TestAssetDepreciationSchedule(FrappeTestCase): ] self.assertEqual(schedules, expected_schedules) + def test_schedule_for_slm_for_existing_asset_daily_pro_rata_enabled(self): + frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1) + asset = create_asset( + calculate_depreciation=1, + depreciation_method="Straight Line", + available_for_use_date="2023-10-10", + is_existing_asset=1, + opening_number_of_booked_depreciations=9, + opening_accumulated_depreciation=265, + depreciation_start_date="2024-07-31", + total_number_of_depreciations=24, + frequency_of_depreciation=1, + gross_purchase_amount=731, + daily_prorata_based=1, + ) + + expected_schedules = [ + ["2024-07-31", 31.0, 296.0], + ["2024-08-31", 31.0, 327.0], + ["2024-09-30", 30.0, 357.0], + ["2024-10-31", 31.0, 388.0], + ["2024-11-30", 30.0, 418.0], + ["2024-12-31", 31.0, 449.0], + ["2025-01-31", 31.0, 480.0], + ["2025-02-28", 28.0, 508.0], + ["2025-03-31", 31.0, 539.0], + ["2025-04-30", 30.0, 569.0], + ["2025-05-31", 31.0, 600.0], + ["2025-06-30", 30.0, 630.0], + ["2025-07-31", 31.0, 661.0], + ["2025-08-31", 31.0, 692.0], + ["2025-09-30", 30.0, 722.0], + ["2025-10-10", 9.0, 731.0], + ] + schedules = [ + [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in get_depr_schedule(asset.name, "Draft") + ] + self.assertEqual(schedules, expected_schedules) + frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0) + + def test_schedule_for_slm_for_existing_asset(self): + asset = create_asset( + calculate_depreciation=1, + depreciation_method="Straight Line", + available_for_use_date="2023-10-10", + is_existing_asset=1, + opening_number_of_booked_depreciations=9, + opening_accumulated_depreciation=265.30, + depreciation_start_date="2024-07-31", + total_number_of_depreciations=24, + frequency_of_depreciation=1, + gross_purchase_amount=731, + ) + + expected_schedules = [ + ["2024-07-31", 30.46, 295.76], + ["2024-08-31", 30.46, 326.22], + ["2024-09-30", 30.46, 356.68], + ["2024-10-31", 30.46, 387.14], + ["2024-11-30", 30.46, 417.6], + ["2024-12-31", 30.46, 448.06], + ["2025-01-31", 30.46, 478.52], + ["2025-02-28", 30.46, 508.98], + ["2025-03-31", 30.46, 539.44], + ["2025-04-30", 30.46, 569.9], + ["2025-05-31", 30.46, 600.36], + ["2025-06-30", 30.46, 630.82], + ["2025-07-31", 30.46, 661.28], + ["2025-08-31", 30.46, 691.74], + ["2025-09-30", 30.46, 722.2], + ["2025-10-10", 8.8, 731.0], + ] + schedules = [ + [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in get_depr_schedule(asset.name, "Draft") + ] + self.assertEqual(schedules, expected_schedules) + + def test_schedule_sl_method_for_existing_asset_with_frequency_of_3_months(self): + asset = create_asset( + calculate_depreciation=1, + depreciation_method="Straight Line", + available_for_use_date="2023-11-01", + is_existing_asset=1, + opening_number_of_booked_depreciations=4, + opening_accumulated_depreciation=223.15, + depreciation_start_date="2024-12-31", + total_number_of_depreciations=12, + frequency_of_depreciation=3, + gross_purchase_amount=731, + ) + + expected_schedules = [ + ["2024-12-31", 60.92, 284.07], + ["2025-03-31", 60.92, 344.99], + ["2025-06-30", 60.92, 405.91], + ["2025-09-30", 60.92, 466.83], + ["2025-12-31", 60.92, 527.75], + ["2026-03-31", 60.92, 588.67], + ["2026-06-30", 60.92, 649.59], + ["2026-09-30", 60.92, 710.51], + ["2026-11-01", 20.49, 731.0], + ] + schedules = [ + [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in get_depr_schedule(asset.name, "Draft") + ] + self.assertEqual(schedules, expected_schedules) + # Enable Checkbox to Calculate depreciation using total days in depreciation period def test_daily_prorata_based_depr_after_enabling_configuration(self): frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 7df96064a0c..da1c70d3179 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -39,16 +39,14 @@ def validate_filters(filters): def get_data(filters): po = frappe.qb.DocType("Purchase Order") po_item = frappe.qb.DocType("Purchase Order Item") - pi = frappe.qb.DocType("Purchase Invoice") pi_item = frappe.qb.DocType("Purchase Invoice Item") query = ( frappe.qb.from_(po) - .from_(po_item) + .inner_join(po_item) + .on(po_item.parent == po.name) .left_join(pi_item) - .on(pi_item.po_detail == po_item.name & pi_item.docstatus == 1) - .left_join(pi) - .on(pi.name == pi_item.parent & pi.docstatus == 1) + .on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1)) .select( po.transaction_date.as_("date"), po_item.schedule_date.as_("required_date"), diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0cbf1b320ca..67bfc71ed5b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2489,16 +2489,12 @@ class AccountsController(TransactionBase): @frappe.whitelist() def repost_accounting_entries(self): - if self.repost_required: - repost_ledger = frappe.new_doc("Repost Accounting Ledger") - repost_ledger.company = self.company - repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) - repost_ledger.flags.ignore_permissions = True - repost_ledger.insert() - repost_ledger.submit() - self.db_set("repost_required", 0) - else: - frappe.throw(_("No updates pending for reposting")) + repost_ledger = frappe.new_doc("Repost Accounting Ledger") + repost_ledger.company = self.company + repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) + repost_ledger.flags.ignore_permissions = True + repost_ledger.insert() + repost_ledger.submit() @frappe.whitelist() diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 76d04c04d6a..a848f6263fb 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -640,6 +640,12 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai def update_terms(source_doc, target_doc, source_parent): target_doc.payment_amount = -source_doc.payment_amount + def item_condition(doc): + if return_against_rejected_qty: + return doc.rejected_qty + + return doc.qty + doclist = get_mapped_doc( doctype, source_name, @@ -654,6 +660,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai "doctype": doctype + " Item", "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"}, "postprocess": update_item, + "condition": item_condition, }, "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, }, diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 028acc9cb11..527be6ab337 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -456,6 +456,7 @@ scheduler_events = { ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", + "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly", ], } diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 71b23ba3202..4062235a32e 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -365,6 +365,12 @@ def get_children(doctype=None, parent=None, **kwargs): return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") +def get_parent_row_no(doc, name): + for row in doc.items: + if row.name == name: + return row.idx + + @frappe.whitelist() def add_item(**kwargs): if isinstance(kwargs, str): @@ -375,6 +381,11 @@ def add_item(**kwargs): doc = frappe.get_doc("BOM Creator", kwargs.parent) item_info = get_item_details(kwargs.item_code) + + parent_row_no = "" + if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id: + parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id) + kwargs.update( { "uom": item_info.stock_uom, @@ -383,6 +394,9 @@ def add_item(**kwargs): } ) + if parent_row_no: + kwargs.update({"parent_row_no": parent_row_no}) + doc.append("items", kwargs) doc.save() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3e9a286b4cf..4615039fbb5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1781,6 +1781,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_price_list(item, reset_plc_conversion) { // We need to reset plc_conversion_rate sometimes because the call to // `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value + + + if (this.frm.doc.doctype === "Material Request") { + return; + } + if (!reset_plc_conversion) { this.frm.set_value("plc_conversion_rate", ""); } diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 787b0826f01..284bd2b7f22 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -754,7 +754,7 @@ "fieldname": "auto_err_frequency", "fieldtype": "Select", "label": "Frequency", - "options": "Daily\nWeekly" + "options": "Daily\nWeekly\nMonthly" }, { "default": "0", @@ -808,7 +808,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-06-21 17:46:25.567565", + "modified": "2024-07-24 18:17:56.413971", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 87d14e3c6b4..f79ea60f5c4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -31,7 +31,7 @@ class Company(NestedSet): accumulated_depreciation_account: DF.Link | None allow_account_creation_against_child_company: DF.Check asset_received_but_not_billed: DF.Link | None - auto_err_frequency: DF.Literal["Daily", "Weekly"] + auto_err_frequency: DF.Literal["Daily", "Weekly", "Monthly"] auto_exchange_rate_revaluation: DF.Check book_advance_payments_in_separate_party_account: DF.Check capital_work_in_progress_account: DF.Link | None diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 689af2c75b8..384673448b0 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -163,7 +163,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.flags.ignore_mandatory = True - doc.insert(ignore_permissions=True) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) return doc @@ -196,7 +196,7 @@ def make_item_tax_template(company_name, template): # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True - doc.insert(ignore_permissions=True) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) return doc @@ -233,7 +233,7 @@ def get_or_create_account(company_name, account): doc = frappe.get_doc(account) doc.flags.ignore_links = True doc.flags.ignore_validate = True - doc.insert(ignore_permissions=True, ignore_mandatory=True) + doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_if_duplicate=True) return doc diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f27db868a07..cfe550f2f27 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 add_days, cstr, flt, nowdate, nowtime, today +from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on @@ -2005,6 +2005,40 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, dn5.submit) + def test_warranty_expiry_date_for_serial_item(self): + item_code = make_item( + "Test Warranty Expiry Date Item", + properties={ + "has_serial_no": 1, + "serial_no_series": "TWE.#####", + "is_stock_item": 1, + "warranty_period": 100, + }, + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=50, + posting_date=nowdate(), + ) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + create_delivery_note( + item_code=item_code, + qty=2, + rate=300, + use_serial_batch_fields=0, + serial_no=serial_nos, + ) + + for row in serial_nos: + sn = frappe.get_doc("Serial No", row) + self.assertEqual(getdate(sn.warranty_expiry_date), getdate(add_days(nowdate(), 100))) + self.assertEqual(sn.status, "Delivered") + self.assertEqual(sn.warranty_period, 100) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 736ced72f33..416a2db43c4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1886,9 +1886,19 @@ class TestPurchaseReceipt(FrappeTestCase): rate=100, rejected_qty=2, rejected_warehouse=rejected_warehouse, + do_not_save=1, ) + pr.append( + "items", + {"item_code": item_code, "qty": 2, "rate": 100, "warehouse": warehouse, "rejected_qty": 0}, + ) + pr.save() + pr.submit() + self.assertEqual(len(pr.items), 2) + pr_return = make_purchase_return_against_rejected_warehouse(pr.name) + self.assertEqual(len(pr_return.items), 1) self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse) self.assertEqual(pr_return.items[0].qty, 2.0 * -1) self.assertEqual(pr_return.items[0].rejected_qty, 0.0) 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 615d98a448d..ef4d475dca4 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 @@ -92,8 +92,10 @@ class SerialandBatchBundle(Document): if self.type_of_transaction == "Maintenance": return - self.validate_serial_nos_duplicate() - self.check_future_entries_exists() + if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: + self.validate_serial_nos_duplicate() + self.check_future_entries_exists() + self.set_is_outward() self.calculate_total_qty() self.set_warehouse() @@ -340,6 +342,9 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: + if (d.incoming_rate == rate) and d.qty and d.stock_value_difference: + continue + d.incoming_rate = flt(rate, precision) if d.qty: d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) @@ -393,32 +398,6 @@ class SerialandBatchBundle(Document): self.calculate_qty_and_amount(save=True) self.validate_quantity(row, qty_field=qty_field) - self.set_warranty_expiry_date() - - def set_warranty_expiry_date(self): - if self.type_of_transaction != "Outward": - return - - if not (self.docstatus == 1 and self.voucher_type == "Delivery Note" and self.has_serial_no): - return - - warranty_period = frappe.get_cached_value("Item", self.item_code, "warranty_period") - - if not warranty_period: - return - - warranty_expiry_date = add_days(self.posting_date, cint(warranty_period)) - - serial_nos = self.get_serial_nos() - if not serial_nos: - return - - sn_table = frappe.qb.DocType("Serial No") - ( - frappe.qb.update(sn_table) - .set(sn_table.warranty_expiry_date, warranty_expiry_date) - .where(sn_table.name.isin(serial_nos)) - ).run() def validate_voucher_no(self): if not (self.voucher_type and self.voucher_no): @@ -867,6 +846,9 @@ class SerialandBatchBundle(Document): self.validate_serial_nos_inventory() def set_purchase_document_no(self): + if self.flags.ignore_validate_serial_batch: + return + if not self.has_serial_no: return diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index a090b37033b..495af7f173a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -83,7 +83,8 @@ "job_card", "amended_from", "credit_note", - "is_return" + "is_return", + "tab_connections" ], "fields": [ { @@ -683,6 +684,12 @@ "label": "Asset Repair", "options": "Asset Repair", "read_only": 1 + }, + { + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_dashboard.py b/erpnext/stock/doctype/stock_entry/stock_entry_dashboard.py new file mode 100644 index 00000000000..c1141fe43ec --- /dev/null +++ b/erpnext/stock/doctype/stock_entry/stock_entry_dashboard.py @@ -0,0 +1,26 @@ +from frappe import _ + + +# Todo: non_standard_fieldnames is to be decided +def get_data(): + return { + "fieldname": "stock_entry", + "non_standard_fieldnames": { + # "DocType Name": "Reference field name", + }, + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Subcontracting Order": ["items", "subcontracting_order"], + "Subcontracting Receipt": ["items", "subcontracting_receipt"], + }, + "transactions": [ + { + "label": _("Reference"), + "items": [ + "Purchase Order", + "Subcontracting Order", + "Subcontracting Receipt", + ], + }, + ], + } diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 431fdf681ff..11c4b1c14f7 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -434,7 +434,6 @@ }, { "default": "1", - "depends_on": "use_serial_batch_fields", "description": "If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial \n / Batch Bundle. ", "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle", "fieldtype": "Check", @@ -460,7 +459,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-15 17:18:23.872161", + "modified": "2024-07-29 14:55:19.093508", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js index 5cef5c70341..f8779c64e2d 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js @@ -3,6 +3,14 @@ frappe.query_reports["Product Bundle Balance"] = { filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, { fieldname: "date", label: __("Date"), diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index dd79e7fcaf5..10f8650b525 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -224,6 +224,9 @@ def get_stock_ledger_entries(filters, items): .where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items))) ) + if filters.get("company"): + query = query.where(sle.company == filters.get("company")) + if date := filters.get("date"): query = query.where(sle.posting_date <= date) else: @@ -237,7 +240,7 @@ def get_stock_ledger_entries(filters, items): if warehouse_details: wh = frappe.qb.DocType("Warehouse") query = query.where( - ExistsCriterion( + sle.warehouse.isin( frappe.qb.from_(wh) .select(wh.name) .where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt)) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 4880adab921..07e46312d3e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,7 +4,7 @@ import frappe from frappe import _, bold from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp -from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today +from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today from pypika import Order from erpnext.stock.deprecated_serial_batch import ( @@ -110,6 +110,7 @@ class SerialBatchBundle: "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward", "company": self.company, "is_rejected": self.is_rejected_entry(), + "make_bundle_from_sle": 1, } ).make_serial_and_batch_bundle() @@ -160,12 +161,13 @@ class SerialBatchBundle: if msg: error_msg = ( - f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}." + f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}" + msg ) frappe.throw(_(error_msg)) def set_serial_and_batch_bundle(self, sn_doc): + self.sle.auto_created_serial_and_batch_bundle = 1 self.sle.db_set({"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1}) if sn_doc.is_rejected: @@ -324,6 +326,9 @@ class SerialBatchBundle: def set_warehouse_and_status_in_serial_nos(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos + if self.sle.auto_created_serial_and_batch_bundle and self.sle.actual_qty > 0: + return + serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle) if not self.sle.serial_and_batch_bundle and self.sle.serial_no: serial_nos = get_parsed_serial_nos(self.sle.serial_no) @@ -338,7 +343,8 @@ class SerialBatchBundle: status = "Delivered" sn_table = frappe.qb.DocType("Serial No") - ( + + query = ( frappe.qb.update(sn_table) .set(sn_table.warehouse, warehouse) .set( @@ -351,7 +357,19 @@ class SerialBatchBundle: ) .set(sn_table.company, self.sle.company) .where(sn_table.name.isin(serial_nos)) - ).run() + ) + + if status == "Delivered": + warranty_period = frappe.get_cached_value("Item", self.sle.item_code, "warranty_period") + if warranty_period: + warranty_expiry_date = add_days(self.sle.posting_date, cint(warranty_period)) + query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date) + query = query.set(sn_table.warranty_period, warranty_period) + else: + query = query.set(sn_table.warranty_expiry_date, None) + query = query.set(sn_table.warranty_period, 0) + + query.run() def set_batch_no_in_serial_nos(self): entries = frappe.get_all( @@ -915,6 +933,10 @@ class SerialBatchCreation: if doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2: doc.voucher_no = "" + doc.flags.ignore_validate_serial_batch = False + if self.get("make_bundle_from_sle") and self.type_of_transaction == "Inward": + doc.flags.ignore_validate_serial_batch = True + doc.save() self.validate_qty(doc) @@ -1107,6 +1129,10 @@ class SerialBatchCreation: msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually." frappe.throw(_(msg)) + voucher_no = "" + if self.get("voucher_no"): + voucher_no = self.get("voucher_no") + for _i in range(abs(cint(self.actual_qty))): serial_no = make_autoname(self.serial_no_series, "Serial No") sr_nos.append(serial_no) @@ -1124,6 +1150,7 @@ class SerialBatchCreation: self.item_name, self.description, "Active", + voucher_no, self.batch_no, ) ) @@ -1142,6 +1169,7 @@ class SerialBatchCreation: "item_name", "description", "status", + "purchase_document_no", "batch_no", ] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e14b0f87501..ac78f1a7f68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -275,7 +275,9 @@ def repost_future_sle( ) affected_transactions.update(obj.affected_transactions) - distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True + key = (args[i].get("item_code"), args[i].get("warehouse")) + if distinct_item_warehouses.get(key): + distinct_item_warehouses[key].reposting_status = True if obj.new_items_found: for _item_wh, data in distinct_item_warehouses.items(): @@ -1588,9 +1590,11 @@ def get_stock_ledger_entries( if not previous_sle.get("posting_date"): previous_sle["posting_datetime"] = "1900-01-01 00:00:00" else: - previous_sle["posting_datetime"] = get_combine_datetime( - previous_sle["posting_date"], previous_sle["posting_time"] - ) + posting_time = previous_sle.get("posting_time") + if not posting_time: + posting_time = "00:00:00" + + previous_sle["posting_datetime"] = get_combine_datetime(previous_sle["posting_date"], posting_time) if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index 507e23365cc..b8bda832983 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -23,18 +23,6 @@ "cost_center", "dimension_col_break", "project", - "address_and_contact_section", - "supplier_address", - "address_display", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "column_break_19", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", "section_break_24", "column_break_25", "set_warehouse", @@ -48,10 +36,23 @@ "raw_materials_supplied_section", "set_reserve_warehouse", "supplied_items", - "additional_costs_section", + "tab_address_and_contact", + "supplier_address", + "address_display", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "column_break_19", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "tab_additional_costs", "distribute_additional_costs_based_on", "additional_costs", "total_additional_costs", + "tab_other_info", "order_status_section", "status", "column_break_39", @@ -59,7 +60,8 @@ "printing_settings_section", "select_print_heading", "column_break_43", - "letter_head" + "letter_head", + "tab_connections" ], "fields": [ { @@ -95,7 +97,7 @@ "fieldtype": "Link", "in_global_search": 1, "in_standard_filter": 1, - "label": "Supplier", + "label": "Job Worker", "options": "Supplier", "print_hide": 1, "reqd": 1, @@ -107,7 +109,7 @@ "fieldname": "supplier_name", "fieldtype": "Data", "in_global_search": 1, - "label": "Supplier Name", + "label": "Job Worker Name", "read_only": 1, "reqd": 1 }, @@ -115,7 +117,7 @@ "depends_on": "supplier", "fieldname": "supplier_warehouse", "fieldtype": "Link", - "label": "Supplier Warehouse", + "label": "Job Worker Warehouse", "options": "Warehouse", "reqd": 1 }, @@ -166,9 +168,8 @@ "read_only": 1 }, { - "collapsible": 1, - "fieldname": "address_and_contact_section", - "fieldtype": "Section Break", + "fieldname": "tab_address_and_contact", + "fieldtype": "Tab Break", "label": "Address and Contact" }, { @@ -176,14 +177,14 @@ "fetch_if_empty": 1, "fieldname": "supplier_address", "fieldtype": "Link", - "label": "Supplier Address", + "label": "Job Worker Address", "options": "Address", "print_hide": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", - "label": "Supplier Address Details", + "label": "Job Worker Address Details", "read_only": 1 }, { @@ -191,7 +192,7 @@ "fetch_if_empty": 1, "fieldname": "contact_person", "fieldtype": "Link", - "label": "Supplier Contact", + "label": "Job Worker Contact", "options": "Contact", "print_hide": 1 }, @@ -337,11 +338,9 @@ "read_only": 1 }, { - "collapsible": 1, - "collapsible_depends_on": "total_additional_costs", "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", - "fieldname": "additional_costs_section", - "fieldtype": "Section Break", + "fieldname": "tab_additional_costs", + "fieldtype": "Tab Break", "label": "Additional Costs" }, { @@ -449,6 +448,17 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 8bcf97da40d..ad03171f29a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -23,18 +23,6 @@ "cost_center", "dimension_col_break", "project", - "section_addresses", - "supplier_address", - "contact_person", - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "col_break_address", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", "sec_warehouse", "set_warehouse", "rejected_warehouse", @@ -53,23 +41,36 @@ "get_current_stock", "raw_material_details", "supplied_items", - "additional_costs_section", - "distribute_additional_costs_based_on", - "additional_costs", - "total_additional_costs", "section_break_46", "in_words", "bill_no", "bill_date", + "tab_addresses", + "supplier_address", + "contact_person", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "col_break_address", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "tab_additional_costs", + "distribute_additional_costs_based_on", + "additional_costs", + "total_additional_costs", + "tab_other_info", "more_info", - "status", - "column_break_39", - "per_returned", - "section_break_47", "amended_from", "range", "column_break4", "represents_company", + "order_status_section", + "status", + "column_break_39", + "per_returned", "subscription_detail", "auto_repeat", "printing_settings", @@ -84,7 +85,8 @@ "transporter_name", "column_break5", "lr_no", - "lr_date" + "lr_date", + "tab_connections" ], "fields": [ { @@ -112,7 +114,7 @@ "fieldname": "supplier", "fieldtype": "Link", "in_global_search": 1, - "label": "Supplier", + "label": "Job Worker", "options": "Supplier", "print_hide": 1, "print_width": "150px", @@ -127,7 +129,7 @@ "fieldname": "supplier_name", "fieldtype": "Data", "in_global_search": 1, - "label": "Supplier Name", + "label": "Job Worker Name", "read_only": 1 }, { @@ -174,15 +176,14 @@ "width": "150px" }, { - "collapsible": 1, - "fieldname": "section_addresses", - "fieldtype": "Section Break", + "fieldname": "tab_addresses", + "fieldtype": "Tab Break", "label": "Address and Contact" }, { "fieldname": "supplier_address", "fieldtype": "Link", - "label": "Select Supplier Address", + "label": "Select Job Worker Address", "options": "Address", "print_hide": 1 }, @@ -269,7 +270,7 @@ { "fieldname": "supplier_warehouse", "fieldtype": "Link", - "label": "Supplier Warehouse", + "label": "Job Worker Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1, @@ -414,6 +415,7 @@ "width": "50%" }, { + "collapsible": 1, "fieldname": "subscription_detail", "fieldtype": "Section Break", "label": "Auto Repeat Detail" @@ -571,10 +573,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "section_break_47", - "fieldtype": "Section Break" - }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", @@ -598,11 +596,9 @@ "options": "Project" }, { - "collapsible": 1, - "collapsible_depends_on": "total_additional_costs", "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", - "fieldname": "additional_costs_section", - "fieldtype": "Section Break", + "fieldname": "tab_additional_costs", + "fieldtype": "Tab Break", "label": "Additional Costs" }, { @@ -643,7 +639,7 @@ { "fieldname": "supplier_delivery_note", "fieldtype": "Data", - "label": "Supplier Delivery Note" + "label": "Job Worker Delivery Note" }, { "fieldname": "raw_materials_consumed_section", @@ -658,6 +654,23 @@ { "fieldname": "column_break_uinr", "fieldtype": "Column Break" + }, + { + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "collapsible": 1, + "fieldname": "order_status_section", + "fieldtype": "Section Break", + "label": "Order Status" + }, + { + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "in_create": 1,