diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 6a475624127..4649d231628 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -89,7 +89,6 @@ class BankTransaction(StatusUpdater): - 0 > a: Error: already over-allocated - clear means: set the latest transaction date as clearance date """ - gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account") remaining_amount = self.unallocated_amount for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 0203c450583..b3ae627c6e2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -154,7 +154,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); - if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) { + if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) { frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() { frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name}); }, __('Actions')); diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 4ae813571af..28c9529995d 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -65,7 +65,8 @@ "fieldtype": "Link", "in_standard_filter": 1, "label": "Voucher Type", - "options": "DocType" + "options": "DocType", + "search_index": 1 }, { "fieldname": "voucher_no", @@ -73,14 +74,16 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Voucher No", - "options": "voucher_type" + "options": "voucher_type", + "search_index": 1 }, { "fieldname": "against_voucher_type", "fieldtype": "Link", "in_standard_filter": 1, "label": "Against Voucher Type", - "options": "DocType" + "options": "DocType", + "search_index": 1 }, { "fieldname": "against_voucher_no", @@ -88,7 +91,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Against Voucher No", - "options": "against_voucher_type" + "options": "against_voucher_type", + "search_index": 1 }, { "fieldname": "amount", @@ -148,13 +152,14 @@ { "fieldname": "voucher_detail_no", "fieldtype": "Data", - "label": "Voucher Detail No" + "label": "Voucher Detail No", + "search_index": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-30 16:15:00.470283", + "modified": "2023-11-03 16:39:58.904113", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3285a529d2d..0c914b351b5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -109,6 +109,8 @@ class PaymentReconciliation(Document): "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" ) + limit = f"limit {self.payment_limit}" if self.payment_limit else " " + # nosemgrep journal_entries = frappe.db.sql( """ @@ -132,11 +134,13 @@ class PaymentReconciliation(Document): ELSE {bank_account_condition} END) order by t1.posting_date + {limit} """.format( **{ "dr_or_cr": dr_or_cr, "bank_account_condition": bank_account_condition, "condition": condition, + "limit": limit, } ), { @@ -162,7 +166,7 @@ class PaymentReconciliation(Document): if self.payment_name: conditions.append(doc.name.like(f"%{self.payment_name}%")) - self.return_invoices = ( + self.return_invoices_query = ( qb.from_(doc) .select( ConstantColumn(voucher_type).as_("voucher_type"), @@ -170,8 +174,11 @@ class PaymentReconciliation(Document): doc.return_against, ) .where(Criterion.all(conditions)) - .run(as_dict=True) ) + if self.payment_limit: + self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit) + + self.return_invoices = self.return_invoices_query.run(as_dict=True) def get_dr_or_cr_notes(self): diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json index 1131a0fca69..b4ac9812cbf 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json @@ -110,7 +110,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-21 17:36:26.642617", + "modified": "2023-11-02 11:32:12.254018", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation Log", @@ -125,7 +125,19 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", "share": 1, "write": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2d1f4451b67..09bffff6da5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -384,7 +384,8 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "fieldname": "column_break_15", @@ -407,7 +408,8 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "section_addresses", @@ -1602,7 +1604,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-10-16 16:24:51.886231", + "modified": "2023-11-03 15:47:30.319200", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1665,4 +1667,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 13593bcf9b4..171cc0ccdf2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): set_advance_flag(company="_Test Company", flag=0, default_account="") def test_gl_entries_for_standalone_debit_note(self): - make_purchase_invoice(qty=5, rate=500, update_stock=True) + from erpnext.stock.doctype.item.test_item import make_item - returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True) + item_code = make_item(properties={"is_stock_item": 1}) + make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True) + + returned_inv = make_purchase_invoice( + item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True + ) # override the rate with valuation rate sle = frappe.get_all( @@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): )[0] rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) - self.assertAlmostEqual(returned_inv.items[0].rate, rate) + self.assertAlmostEqual(rate, 500) def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( @@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): disable_dimension() def test_repost_accounting_entries(self): + # update repost settings + settings = frappe.get_doc("Repost Accounting Ledger Settings") + if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]: + settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True}) + settings.save() + pi = make_purchase_invoice( rate=1000, price_list_rate=1000, diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js index 3a87a380d19..c7b7a148cfb 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js @@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", { setup: function(frm) { frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { return { - filters: { - name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']], - } + query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types" } } diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index dbb0971fdea..69cfe9fcd74 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -10,9 +10,12 @@ from frappe.utils.data import comma_and class RepostAccountingLedger(Document): def __init__(self, *args, **kwargs): super(RepostAccountingLedger, self).__init__(*args, **kwargs) - self._allowed_types = set( - ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"] - ) + self._allowed_types = [ + x.document_type + for x in frappe.db.get_all( + "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] + ) + ] def validate(self): self.validate_vouchers() @@ -157,7 +160,7 @@ def start_repost(account_repost_doc=str) -> None: doc.docstatus = 1 doc.make_gl_entries() - elif doc.doctype in ["Payment Entry", "Journal Entry"]: + elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]: if not repost_doc.delete_cancelled_entries: doc.make_gl_entries(1) doc.make_gl_entries() @@ -186,3 +189,18 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) ) ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters): + filters = {"allowed": True} + + if txt: + filters.update({"document_type": ("like", f"%{txt}%")}) + + if allowed_types := frappe.db.get_all( + "Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1 + ): + return allowed_types + return [] diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index 0e75dd2e3e1..dda0ec778f6 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): self.create_company() self.create_customer() self.create_item() + self.update_repost_settings() def teadDown(self): frappe.db.rollback() + def update_repost_settings(self): + allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") + for x in allowed_types: + repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) + repost_settings.save() + def test_01_basic_functions(self): si = create_sales_invoice( item=self.item, diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js new file mode 100644 index 00000000000..8c83ca50431 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Repost Accounting Ledger Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json new file mode 100644 index 00000000000..8aa0a840c7e --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2023-11-07 09:57:20.619939", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "allowed_types" + ], + "fields": [ + { + "fieldname": "allowed_types", + "fieldtype": "Table", + "label": "Allowed Doctypes", + "options": "Repost Allowed Types" + } + ], + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2023-11-07 14:24:13.321522", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "System Manager", + "select": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py new file mode 100644 index 00000000000..2b8230df86f --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAccountingLedgerSettings(Document): + pass diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py new file mode 100644 index 00000000000..ec4e87ffc00 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRepostAccountingLedgerSettings(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/repost_allowed_types/__init__.py b/erpnext/accounts/doctype/repost_allowed_types/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json new file mode 100644 index 00000000000..ede12fbc185 --- /dev/null +++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-11-07 09:58:03.595382", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_sfzb", + "allowed" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Doctype", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "allowed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allowed" + }, + { + "fieldname": "column_break_sfzb", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-11-07 10:01:39.217861", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Allowed Types", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py new file mode 100644 index 00000000000..0e4883b0c93 --- /dev/null +++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAllowedTypes(Document): + pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e5adeae501b..cd725b98622 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -26,6 +26,7 @@ "is_return", "return_against", "update_billed_amount_in_sales_order", + "update_billed_amount_in_delivery_note", "is_debit_note", "amended_from", "accounting_dimensions_section", @@ -2153,6 +2154,13 @@ "fieldname": "use_company_roundoff_cost_center", "fieldtype": "Check", "label": "Use Company default Cost Center for Round off" + }, + { + "default": "0", + "depends_on": "eval: doc.is_return", + "fieldname": "update_billed_amount_in_delivery_note", + "fieldtype": "Check", + "label": "Update Billed Amount in Delivery Note" } ], "icon": "fa fa-file-text", @@ -2165,7 +2173,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-07-25 16:02:18.988799", + "modified": "2023-11-03 14:39:38.012346", "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 f6d9c93261a..fa95ccdc57e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -253,6 +253,7 @@ class SalesInvoice(SellingController): self.update_status_updater_args() self.update_prevdoc_status() + self.update_billing_status_in_dn() self.clear_unallocated_mode_of_payments() @@ -1019,7 +1020,7 @@ class SalesInvoice(SellingController): def make_customer_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total - # because rounded_total had value even before introcution of posting GLE based on rounded total + # because rounded_total had value even before introduction of posting GLE based on rounded total grand_total = ( self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total ) @@ -1267,7 +1268,7 @@ class SalesInvoice(SellingController): if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: payment_mode.base_amount -= flt(self.change_amount) - if payment_mode.amount: + if payment_mode.base_amount: # POS, make payment entries gl_entries.append( self.get_gl_dict( @@ -1429,6 +1430,8 @@ class SalesInvoice(SellingController): ) def update_billing_status_in_dn(self, update_modified=True): + if self.is_return and not self.update_billed_amount_in_delivery_note: + return updated_delivery_notes = [] for d in self.get("items"): if d.dn_detail: diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 9c73cbb344f..eff705dafac 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -143,7 +143,13 @@ frappe.query_reports["Accounts Payable"] = { "fieldname": "show_future_payments", "label": __("Show Future Payments"), "fieldtype": "Check", + }, + { + "fieldname": "ignore_accounts", + "label": __("Group by Voucher"), + "fieldtype": "Check", } + ], "formatter": function(value, row, column, data, default_formatter) { @@ -175,4 +181,4 @@ function get_party_type_options() { }); }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 1073be0bdc4..786aad601ba 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -172,7 +172,13 @@ frappe.query_reports["Accounts Receivable"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check", + }, + { + "fieldname": "ignore_accounts", + "label": __("Group by Voucher"), + "fieldtype": "Check", } + ], "formatter": function(value, row, column, data, default_formatter) { @@ -205,4 +211,4 @@ function get_party_type_options() { }); }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 20444f94964..12e40037f1e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -116,7 +116,12 @@ class ReceivablePayableReport(object): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: # get the balance object for voucher_type - key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) + + if self.filters.get("ignore_accounts"): + key = (ple.voucher_type, ple.voucher_no, ple.party) + else: + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) + if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, @@ -183,7 +188,10 @@ class ReceivablePayableReport(object): ): return - key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) + if self.filters.get("ignore_accounts"): + key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + else: + key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) # If payment is made against credit note # and credit note is made against a Sales Invoice @@ -192,13 +200,19 @@ class ReceivablePayableReport(object): if ple.against_voucher_no in self.return_entries: return_against = self.return_entries.get(ple.against_voucher_no) if return_against: - key = (ple.account, ple.against_voucher_type, return_against, ple.party) + if self.filters.get("ignore_accounts"): + key = (ple.against_voucher_type, return_against, ple.party) + else: + key = (ple.account, ple.against_voucher_type, return_against, ple.party) row = self.voucher_balance.get(key) if not row: # no invoice, this is an invoice / stand-alone payment / credit note - row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) + if self.filters.get("ignore_accounts"): + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + else: + row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row.party_type = ple.party_type return row diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 099884a48ec..696a03b0a70 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -79,7 +79,9 @@ class General_Payment_Ledger_Comparison(object): .select( gle.company, gle.account, + gle.voucher_type, gle.voucher_no, + gle.party_type, gle.party, outstanding, ) @@ -89,7 +91,9 @@ class General_Payment_Ledger_Comparison(object): & (gle.account.isin(val.accounts)) ) .where(Criterion.all(filter_criterion)) - .groupby(gle.company, gle.account, gle.voucher_no, gle.party) + .groupby( + gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party + ) .run() ) @@ -112,7 +116,13 @@ class General_Payment_Ledger_Comparison(object): self.account_types[acc_type].ple = ( qb.from_(ple) .select( - ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding") + ple.company, + ple.account, + ple.voucher_type, + ple.voucher_no, + ple.party_type, + ple.party, + Sum(ple.amount).as_("outstanding"), ) .where( (ple.company == self.filters.company) @@ -120,7 +130,9 @@ class General_Payment_Ledger_Comparison(object): & (ple.account.isin(val.accounts)) ) .where(Criterion.all(filter_criterion)) - .groupby(ple.company, ple.account, ple.voucher_no, ple.party) + .groupby( + ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party + ) .run() ) @@ -138,12 +150,12 @@ class General_Payment_Ledger_Comparison(object): self.diff = frappe._dict({}) for x in self.variation_in_payment_ledger: - self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) + self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]}) for x in self.variation_in_general_ledger: - self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update( - frappe._dict({"pl_balance": x[4]}) - ) + self.diff.setdefault( + (x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0}) + ).update(frappe._dict({"pl_balance": x[6]})) def generate_data(self): self.data = [] @@ -151,8 +163,12 @@ class General_Payment_Ledger_Comparison(object): self.data.append( frappe._dict( { - "voucher_no": key[2], - "party": key[3], + "company": key[0], + "account": key[1], + "voucher_type": key[2], + "voucher_no": key[3], + "party_type": key[4], + "party": key[5], "gl_balance": val.gl_balance, "pl_balance": val.pl_balance, } @@ -162,12 +178,52 @@ class General_Payment_Ledger_Comparison(object): def get_columns(self): self.columns = [] options = None + self.columns.append( + dict( + label=_("Company"), + fieldname="company", + fieldtype="Link", + options="Company", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Account"), + fieldname="account", + fieldtype="Link", + options="Account", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Voucher Type"), + fieldname="voucher_type", + fieldtype="Link", + options="DocType", + width="100", + ) + ) + self.columns.append( dict( label=_("Voucher No"), fieldname="voucher_no", - fieldtype="Data", - options=options, + fieldtype="Dynamic Link", + options="voucher_type", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Party Type"), + fieldname="party_type", + fieldtype="Link", + options="DocType", width="100", ) ) @@ -176,8 +232,8 @@ class General_Payment_Ledger_Comparison(object): dict( label=_("Party"), fieldname="party", - fieldtype="Data", - options=options, + fieldtype="Dynamic Link", + options="party_type", width="100", ) ) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py index 4b0e99d7125..59e906ba332 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -50,7 +50,11 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin): self.assertEqual(len(data), 1) expected = { + "company": sinv.company, + "account": sinv.debit_to, + "voucher_type": sinv.doctype, "voucher_no": sinv.name, + "party_type": "Customer", "party": sinv.customer, "gl_balance": sinv.grand_total, "pl_balance": sinv.grand_total - 1, diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 37d0659acfc..c0b4f595794 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -193,7 +193,13 @@ frappe.query_reports["General Ledger"] = { "fieldname": "add_values_in_transaction_currency", "label": __("Add Columns in Transaction Currency"), "fieldtype": "Check" + }, + { + "fieldname": "show_remarks", + "label": __("Show Remarks"), + "fieldtype": "Check" } + ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 79bfd7833ab..5e484cf5584 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -163,6 +163,9 @@ def get_gl_entries(filters, accounting_dimensions): select_fields = """, debit, credit, debit_in_account_currency, credit_in_account_currency """ + if filters.get("show_remarks"): + select_fields += """,remarks""" + order_by_statement = "order by posting_date, account, creation" if filters.get("include_dimensions"): @@ -195,7 +198,7 @@ def get_gl_entries(filters, accounting_dimensions): voucher_type, voucher_no, {dimension_fields} cost_center, project, {transaction_currency_fields} against_voucher_type, against_voucher, account_currency, - remarks, against, is_opening, creation {select_fields} + against, is_opening, creation {select_fields} from `tabGL Entry` where company=%(company)s {conditions} {order_by_statement} @@ -631,8 +634,10 @@ def get_columns(filters): "width": 100, }, {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, - {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, ] ) + if filters.get("show_remarks"): + columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}]) + return columns diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index f2ec31c70e1..2ba5ce0210a 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -47,6 +47,7 @@ def get_result( out = [] for name, details in gle_map.items(): tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + bill_no, bill_date = "", "" tax_withholding_category = tax_category_map.get(name) rate = tax_rate_map.get(tax_withholding_category) @@ -70,7 +71,10 @@ def get_result( if net_total_map.get(name): if voucher_type == "Journal Entry": # back calcalute total amount from rate and tax_amount - total_amount = grand_total = base_total = tax_amount / (rate / 100) + if rate: + total_amount = grand_total = base_total = tax_amount / (rate / 100) + elif voucher_type == "Purchase Invoice": + total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name) else: total_amount, grand_total, base_total = net_total_map.get(name) else: @@ -96,7 +100,7 @@ def get_result( row.update( { - "section_code": tax_withholding_category, + "section_code": tax_withholding_category or "", "entity_type": party_map.get(party, {}).get(party_type), "rate": rate, "total_amount": total_amount, @@ -106,10 +110,14 @@ def get_result( "transaction_date": posting_date, "transaction_type": voucher_type, "ref_no": name, + "supplier_invoice_no": bill_no, + "supplier_invoice_date": bill_date, } ) out.append(row) + out.sort(key=lambda x: x["section_code"]) + return out @@ -157,14 +165,14 @@ def get_gle_map(documents): def get_columns(filters): pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ - {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, { - "label": _(filters.get("party_type")), - "fieldname": "party", - "fieldtype": "Dynamic Link", - "options": "party_type", - "width": 180, + "label": _("Section Code"), + "options": "Tax Withholding Category", + "fieldname": "section_code", + "fieldtype": "Link", + "width": 90, }, + {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, ] if filters.naming_series == "Naming Series": @@ -179,51 +187,60 @@ def get_columns(filters): columns.extend( [ - { - "label": _("Date of Transaction"), - "fieldname": "transaction_date", - "fieldtype": "Date", - "width": 100, - }, - { - "label": _("Section Code"), - "options": "Tax Withholding Category", - "fieldname": "section_code", - "fieldtype": "Link", - "width": 90, - }, {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100}, - { - "label": _("Total Amount"), - "fieldname": "total_amount", - "fieldtype": "Float", - "width": 90, - }, + ] + ) + if filters.party_type == "Supplier": + columns.extend( + [ + { + "label": _("Supplier Invoice No"), + "fieldname": "supplier_invoice_no", + "fieldtype": "Data", + "width": 120, + }, + { + "label": _("Supplier Invoice Date"), + "fieldname": "supplier_invoice_date", + "fieldtype": "Date", + "width": 120, + }, + ] + ) + + columns.extend( + [ { "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), "fieldname": "rate", "fieldtype": "Percent", - "width": 90, + "width": 60, }, { - "label": _("Tax Amount"), - "fieldname": "tax_amount", + "label": _("Total Amount"), + "fieldname": "total_amount", "fieldtype": "Float", - "width": 90, - }, - { - "label": _("Grand Total"), - "fieldname": "grand_total", - "fieldtype": "Float", - "width": 90, + "width": 120, }, { "label": _("Base Total"), "fieldname": "base_total", "fieldtype": "Float", - "width": 90, + "width": 120, }, - {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100}, + { + "label": _("Tax Amount"), + "fieldname": "tax_amount", + "fieldtype": "Float", + "width": 120, + }, + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Float", + "width": 120, + }, + {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130}, { "label": _("Reference No."), "fieldname": "ref_no", @@ -231,6 +248,12 @@ def get_columns(filters): "options": "transaction_type", "width": 180, }, + { + "label": _("Date of Transaction"), + "fieldname": "transaction_date", + "fieldtype": "Date", + "width": 100, + }, ] ) @@ -253,27 +276,7 @@ def get_tds_docs(filters): "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" ) - query_filters = { - "account": ("in", tds_accounts), - "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), - "is_cancelled": 0, - "against": ("not in", bank_accounts), - } - - party = frappe.get_all(filters.get("party_type"), pluck="name") - or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"}) - - if filters.get("party"): - del query_filters["account"] - del query_filters["against"] - or_filters = {"against": filters.get("party"), "party": filters.get("party")} - - tds_docs = frappe.get_all( - "GL Entry", - filters=query_filters, - or_filters=or_filters, - fields=["voucher_no", "voucher_type", "against", "party"], - ) + tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) for d in tds_docs: if d.voucher_type == "Purchase Invoice": @@ -309,6 +312,47 @@ def get_tds_docs(filters): ) +def get_tds_docs_query(filters, bank_accounts, tds_accounts): + if not tds_accounts: + frappe.throw( + _("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), + title="Accounts Missing Error", + ) + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select("voucher_no", "voucher_type", "against", "party") + .where((gle.is_cancelled == 0)) + ) + + if filters.get("from_date"): + query = query.where(gle.posting_date >= filters.get("from_date")) + if filters.get("to_date"): + query = query.where(gle.posting_date <= filters.get("to_date")) + + if bank_accounts: + query = query.where(gle.against.notin(bank_accounts)) + + if filters.get("party"): + party = [filters.get("party")] + query = query.where( + ((gle.account.isin(tds_accounts) & gle.against.isin(party))) + | ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))) + | gle.party.isin(party) + ) + else: + party = frappe.get_all(filters.get("party_type"), pluck="name") + query = query.where( + ((gle.account.isin(tds_accounts) & gle.against.isin(party))) + | ( + (gle.voucher_type == "Journal Entry") + & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) + ) + | gle.party.isin(party) + ) + return query + + def get_journal_entry_party_map(journal_entries): journal_entry_party_map = {} for d in frappe.db.get_all( @@ -335,6 +379,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "base_tax_withholding_net_total", "grand_total", "base_total", + "bill_no", + "bill_date", ], "Sales Invoice": ["base_net_total", "grand_total", "base_total"], "Payment Entry": [ @@ -353,7 +399,13 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total] + value = [ + entry.base_tax_withholding_net_total, + entry.grand_total, + entry.base_total, + entry.bill_no, + entry.bill_date, + ] elif doctype == "Sales Invoice": value = [entry.base_net_total, entry.grand_total, entry.base_total] elif doctype == "Payment Entry": diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9d356349330..3c570d1af0f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -818,7 +818,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "depreciation_method": d.depreciation_method, "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, - "daily_depreciation": d.daily_depreciation, + "daily_prorata_based": d.daily_prorata_based, "salvage_value_percentage": d.salvage_value_percentage, "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index e2a4b2909a5..84a428ca541 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -780,6 +780,15 @@ def get_disposal_account_and_cost_center(company): def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): asset_doc = frappe.get_doc("Asset", asset) + if asset_doc.available_for_use_date > getdate(disposal_date): + frappe.throw( + "Disposal date {0} cannot be before available for use date {1} of the asset.".format( + disposal_date, asset_doc.available_for_use_date + ) + ) + elif asset_doc.available_for_use_date == getdate(disposal_date): + return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation) + if not asset_doc.calculate_depreciation: return flt(asset_doc.value_after_depreciation) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 88ef69cddc3..04adca45952 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -755,7 +755,9 @@ class TestDepreciationMethods(AssetSetup): self.assertEqual(schedules, expected_schedules) - def test_schedule_for_straight_line_method_with_daily_depreciation(self): + def test_schedule_for_straight_line_method_with_daily_prorata_based( + self, + ): asset = create_asset( calculate_depreciation=1, available_for_use_date="2023-01-01", @@ -764,7 +766,7 @@ class TestDepreciationMethods(AssetSetup): depreciation_start_date="2023-01-31", total_number_of_depreciations=12, frequency_of_depreciation=1, - daily_depreciation=1, + daily_prorata_based=1, ) expected_schedules = [ @@ -1760,7 +1762,7 @@ def create_asset(**args): "total_number_of_depreciations": args.total_number_of_depreciations or 5, "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, - "daily_depreciation": args.daily_depreciation or 0, + "daily_prorata_based": args.daily_prorata_based or 0, }, ) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index 3772ef4d683..8d8b46321fc 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -19,7 +19,7 @@ "depreciation_method", "total_number_of_depreciations", "rate_of_depreciation", - "daily_depreciation", + "daily_prorata_based", "column_break_8", "frequency_of_depreciation", "expected_value_after_useful_life", @@ -179,9 +179,9 @@ { "default": "0", "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", - "fieldname": "daily_depreciation", + "fieldname": "daily_prorata_based", "fieldtype": "Check", - "label": "Daily Depreciation", + "label": "Depreciate based on daily pro-rata", "print_hide": 1, "read_only": 1 } @@ -189,7 +189,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-10 22:22:09.722968", + "modified": "2023-11-03 21:32:15.021796", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", 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 7a88ffc5b7f..7305691f97c 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -153,7 +153,7 @@ class AssetDepreciationSchedule(Document): self.frequency_of_depreciation = row.frequency_of_depreciation self.rate_of_depreciation = row.rate_of_depreciation self.expected_value_after_useful_life = row.expected_value_after_useful_life - self.daily_depreciation = row.daily_depreciation + self.daily_prorata_based = row.daily_prorata_based self.status = "Draft" def make_depr_schedule( @@ -573,7 +573,7 @@ def get_straight_line_or_manual_depr_amount( ) # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: - if row.daily_depreciation: + if row.daily_prorata_based: daily_depr_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( @@ -618,7 +618,7 @@ def get_straight_line_or_manual_depr_amount( ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: - if row.daily_depreciation: + if row.daily_prorata_based: daily_depr_amount = ( flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 2c27dc9aca4..e597d5fe31e 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -8,7 +8,7 @@ "finance_book", "depreciation_method", "total_number_of_depreciations", - "daily_depreciation", + "daily_prorata_based", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -86,23 +86,23 @@ "fieldtype": "Percent", "label": "Rate of Depreciation" }, - { - "default": "0", - "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", - "fieldname": "daily_depreciation", - "fieldtype": "Check", - "label": "Daily Depreciation" - }, { "fieldname": "salvage_value_percentage", "fieldtype": "Percent", "label": "Salvage Value Percentage" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", + "fieldname": "daily_prorata_based", + "fieldtype": "Check", + "label": "Depreciate based on daily pro-rata" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-29 15:39:52.740594", + "modified": "2023-11-03 21:30:24.266601", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index b1da97d6346..2b6ffb752ff 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -470,6 +470,7 @@ "fieldname": "material_request", "fieldtype": "Link", "label": "Material Request", + "mandatory_depends_on": "eval: doc.material_request_item", "no_copy": 1, "oldfieldname": "prevdoc_docname", "oldfieldtype": "Link", @@ -485,6 +486,7 @@ "fieldtype": "Data", "hidden": 1, "label": "Material Request Item", + "mandatory_depends_on": "eval: doc.material_request", "no_copy": 1, "oldfieldname": "prevdoc_detail_docname", "oldfieldtype": "Data", @@ -914,7 +916,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-27 15:50:42.655573", + "modified": "2023-11-06 11:00:53.596417", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index f37db5f1150..60dd54c2385 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -174,7 +174,7 @@ "fieldname": "supplier_type", "fieldtype": "Select", "label": "Supplier Type", - "options": "Company\nIndividual", + "options": "Company\nIndividual\nProprietorship\nPartnership", "reqd": 1 }, { @@ -485,7 +485,7 @@ "link_fieldname": "party" } ], - "modified": "2023-09-25 12:48:21.869563", + "modified": "2023-10-19 16:55:15.148325", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cc5d643c147..35288617ad5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2267,6 +2267,7 @@ class AccountsController(TransactionBase): 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) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a76abe2154b..ece08d83f9d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -4,7 +4,7 @@ import frappe from frappe import ValidationError, _, msgprint -from frappe.contacts.doctype.address.address import get_address_display +from frappe.contacts.doctype.address.address import render_address from frappe.utils import cint, flt, getdate from frappe.utils.data import nowtime @@ -105,26 +105,26 @@ class BuyingController(SubcontractingController): def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: for row in self.items: + if row.rate <= 0: + # override the rate with valuation rate + row.rate = get_incoming_rate( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.get("posting_date"), + "posting_time": self.get("posting_time"), + "qty": row.qty, + "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + }, + raise_error_if_no_rate=False, + ) - # override the rate with valuation rate - row.rate = get_incoming_rate( - { - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.get("posting_date"), - "posting_time": self.get("posting_time"), - "qty": row.qty, - "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - }, - raise_error_if_no_rate=False, - ) - - row.discount_percentage = 0.0 - row.discount_amount = 0.0 - row.margin_rate_or_amount = 0.0 + row.discount_percentage = 0.0 + row.discount_amount = 0.0 + row.margin_rate_or_amount = 0.0 def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -246,7 +246,9 @@ class BuyingController(SubcontractingController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def set_total_in_words(self): from frappe.utils import money_in_words diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 73a248fb531..d09001c8fc1 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -47,15 +47,15 @@ status_map = { ], [ "To Bill", - "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1", + "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1", ], [ "To Deliver", - "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note", + "eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note", ], [ "Completed", - "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", + "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1", ], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index c19e56d6e2a..e48c01b77fc 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1213,8 +1213,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa repost_entry = frappe.new_doc("Repost Item Valuation") repost_entry.based_on = "Item and Warehouse" - repost_entry.voucher_type = voucher_type - repost_entry.voucher_no = voucher_no repost_entry.item_code = sle.item_code repost_entry.warehouse = sle.warehouse diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 11d5f6a9c4d..eb99345991c 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.desk.doctype.tag.tag import add_tag from frappe.model.document import Document -from frappe.utils import add_months, formatdate, getdate, today +from frappe.utils import add_months, formatdate, getdate, sbool, today from plaid.errors import ItemError from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account @@ -237,8 +237,6 @@ def new_bank_transaction(transaction): deposit = abs(amount) withdrawal = 0.0 - status = "Pending" if transaction["pending"] == True else "Settled" - tags = [] if transaction["category"]: try: @@ -247,13 +245,14 @@ def new_bank_transaction(transaction): except KeyError: pass - if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): + if not frappe.db.exists( + "Bank Transaction", dict(transaction_id=transaction["transaction_id"]) + ) and not sbool(transaction["pending"]): try: new_transaction = frappe.get_doc( { "doctype": "Bank Transaction", "date": getdate(transaction["date"]), - "status": status, "bank_account": bank_account, "deposit": deposit, "withdrawal": withdrawal, diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 4a0041662bc..49386c4ebc4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -36,6 +36,7 @@ "prod_plan_references", "section_break_24", "combine_sub_items", + "sub_assembly_warehouse", "section_break_ucc4", "skip_available_sub_assembly_item", "column_break_igxl", @@ -416,13 +417,19 @@ { "fieldname": "column_break_igxl", "fieldtype": "Column Break" + }, + { + "fieldname": "sub_assembly_warehouse", + "fieldtype": "Link", + "label": "Sub Assembly Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-09-29 11:41:03.246059", + "modified": "2023-11-03 14:08:11.928027", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1850d1e09e1..6b12a29b508 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -490,6 +490,12 @@ class ProductionPlan(Document): bin = frappe.get_doc("Bin", bin_name, for_update=True) bin.update_reserved_qty_for_production_plan() + for d in self.sub_assembly_items: + if d.fg_warehouse and d.type_of_manufacturing == "In House": + bin_name = get_or_make_bin(d.production_item, d.fg_warehouse) + bin = frappe.get_doc("Bin", bin_name, for_update=True) + bin.update_reserved_qty_for_for_sub_assembly() + def delete_draft_work_order(self): for d in frappe.get_all( "Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)} @@ -809,7 +815,11 @@ class ProductionPlan(Document): bom_data = [] - warehouse = row.warehouse if self.skip_available_sub_assembly_item else None + warehouse = ( + (self.sub_assembly_warehouse or row.warehouse) + if self.skip_available_sub_assembly_item + else None + ) get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) @@ -831,7 +841,7 @@ class ProductionPlan(Document): for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name - data.fg_warehouse = row.warehouse + data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse data.schedule_date = row.planned_start_date data.type_of_manufacturing = manufacturing_type or ( "Subcontract" if data.is_sub_contracted_item else "In House" @@ -1637,8 +1647,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): query = query.run() - if not query: - return 0.0 + if not query or query[0][0] is None: + return None reserved_qty_for_production_plan = flt(query[0][0]) @@ -1780,3 +1790,29 @@ def sales_order_query( query = query.offset(start) return query.run() + + +def get_reserved_qty_for_sub_assembly(item_code, warehouse): + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Production Plan Sub Assembly Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(Sum(child.qty - IfNull(child.wo_produced_qty, 0))) + .where( + (table.docstatus == 1) + & (child.production_item == item_code) + & (child.fg_warehouse == warehouse) + & (table.status.notin(["Completed", "Closed"])) + ) + ) + + query = query.run() + + if not query or query[0][0] is None: + return None + + qty = flt(query[0][0]) + return qty if qty > 0 else 0.0 diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index d414988f411..e9c6ee3af20 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1042,13 +1042,14 @@ class TestProductionPlan(FrappeTestCase): after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertEqual(after_qty - before_qty, 1) - pln = frappe.get_doc("Production Plan", pln.name) pln.cancel() bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + pln.reload() + self.assertEqual(pln.docstatus, 2) self.assertEqual(after_qty, before_qty) def test_resered_qty_for_production_plan_for_work_order(self): @@ -1359,6 +1360,93 @@ class TestProductionPlan(FrappeTestCase): if row.item_code == "ChildPart2 For SUB Test": self.assertEqual(row.quantity, 2) + def test_reserve_sub_assembly_items(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + bom_tree = { + "Fininshed Goods Bicycle": { + "Frame Assembly": {"Frame": {}}, + "Chain Assembly": {"Chain": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + company = "_Test Company" + + sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company) + + for item_code in ["Frame", "Chain"]: + make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100) + + before_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=2, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse=warehouse, + sub_assembly_warehouse=sub_assembly_warehouse, + ) + + plan.get_sub_assembly_items() + plan.submit() + + after_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + self.assertEqual(after_qty, before_qty + 2) + + plan.make_work_order() + work_orders = frappe.get_all( + "Work Order", + fields=["name", "production_item"], + filters={"production_plan": plan.name}, + order_by="creation desc", + ) + + for d in work_orders: + wo_doc = frappe.get_doc("Work Order", d.name) + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + + wo_doc.wip_warehouse = ( + warehouse + if d.production_item in ["Frame Assembly", "Chain Assembly"] + else sub_assembly_warehouse + ) + + wo_doc.submit() + + if d.production_item == "Frame Assembly": + self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse) + se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2)) + se_doc.submit() + + after_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + self.assertEqual(after_qty, before_qty) + def create_production_plan(**args): """ @@ -1379,6 +1467,7 @@ def create_production_plan(**args): "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, "get_items_from": "Sales Order", "skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0, + "sub_assembly_warehouse": args.sub_assembly_warehouse, } ) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index fde0404c019..aff740b732e 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -17,11 +17,10 @@ "type_of_manufacturing", "supplier", "work_order_details_section", - "work_order", + "wo_produced_qty", "purchase_order", "production_plan_item", "column_break_7", - "produced_qty", "received_qty", "indent", "section_break_19", @@ -52,13 +51,6 @@ "fieldtype": "Section Break", "label": "Reference" }, - { - "fieldname": "work_order", - "fieldtype": "Link", - "label": "Work Order", - "options": "Work Order", - "read_only": 1 - }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -81,7 +73,8 @@ { "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Qty" + "label": "Received Qty", + "read_only": 1 }, { "fieldname": "bom_no", @@ -161,12 +154,6 @@ "label": "Target Warehouse", "options": "Warehouse" }, - { - "fieldname": "produced_qty", - "fieldtype": "Data", - "label": "Produced Quantity", - "read_only": 1 - }, { "default": "In House", "fieldname": "type_of_manufacturing", @@ -209,12 +196,18 @@ "label": "Projected Qty", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "wo_produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-22 17:52:34.708879", + "modified": "2023-11-03 13:33:42.959387", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 58945bba77d..d9cc212e8c7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -710,7 +710,7 @@ erpnext.work_order = { return new Promise((resolve, reject) => { frappe.prompt({ fieldtype: 'Float', - label: __('Qty for {0}', [purpose]), + label: __('Qty for {0}', [__(purpose)]), fieldname: 'qty', description: __('Max: {0}', [max]), default: max diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f9fddcbb5e9..36a0cae5cca 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -293,6 +293,7 @@ class WorkOrder(Document): update_produced_qty_in_so_item(self.sales_order, self.sales_order_item) if self.production_plan: + self.set_produced_qty_for_sub_assembly_item() self.update_production_plan_status() def get_transferred_or_manufactured_qty(self, purpose): @@ -569,16 +570,49 @@ class WorkOrder(Document): ) def update_planned_qty(self): + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_sub_assembly, + ) + + qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)} + + if self.production_plan_sub_assembly_item and self.production_plan: + qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly( + self.production_item, self.fg_warehouse + ) + update_bin_qty( self.production_item, self.fg_warehouse, - {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}, + qty_dict, ) if self.material_request: mr_obj = frappe.get_doc("Material Request", self.material_request) mr_obj.update_requested_qty([self.material_request_item]) + def set_produced_qty_for_sub_assembly_item(self): + table = frappe.qb.DocType("Work Order") + + query = ( + frappe.qb.from_(table) + .select(Sum(table.produced_qty)) + .where( + (table.production_plan == self.production_plan) + & (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item) + & (table.docstatus == 1) + ) + ).run() + + produced_qty = flt(query[0][0]) if query else 0 + + frappe.db.set_value( + "Production Plan Sub Assembly Item", + self.production_plan_sub_assembly_item, + "wo_produced_qty", + produced_qty, + ) + def update_ordered_qty(self): if ( self.production_plan diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 78d2c2c3408..d394db6d961 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -345,5 +345,9 @@ erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) +erpnext.patches.v14_0.add_default_for_repost_settings +erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month +erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based +erpnext.patches.v15_0.set_reserved_stock_in_bin # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index efbb96c100a..e53bdf8f19e 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -3,23 +3,24 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "quality_inspection_parameter") + params = set() - # get all distinct parameters from QI readigs table - reading_params = frappe.db.get_all( - "Quality Inspection Reading", fields=["distinct specification"] - ) - reading_params = [d.specification for d in reading_params] + # get all parameters from QI readings table + for (p,) in frappe.db.get_all( + "Quality Inspection Reading", fields=["specification"], as_list=True + ): + params.add(p.strip()) - # get all distinct parameters from QI Template as some may be unused in QI - template_params = frappe.db.get_all( - "Item Quality Inspection Parameter", fields=["distinct specification"] - ) - template_params = [d.specification for d in template_params] + # get all parameters from QI Template as some may be unused in QI + for (p,) in frappe.db.get_all( + "Item Quality Inspection Parameter", fields=["specification"], as_list=True + ): + params.add(p.strip()) - params = list(set(reading_params + template_params)) + # because db primary keys are case insensitive, so duplicates will cause an exception + params = set({x.casefold(): x for x in params}.values()) for parameter in params: - if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc( - {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} - ).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v14_0/add_default_for_repost_settings.py b/erpnext/patches/v14_0/add_default_for_repost_settings.py new file mode 100644 index 00000000000..6cafc66aab8 --- /dev/null +++ b/erpnext/patches/v14_0/add_default_for_repost_settings.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + """ + Update Repost Accounting Ledger Settings with default values + """ + allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") + for x in allowed_types: + repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) + repost_settings.save() diff --git a/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py b/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py new file mode 100644 index 00000000000..63dc0e09bc9 --- /dev/null +++ b/erpnext/patches/v15_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.utils.rename_field import rename_field + + +def execute(): + try: + rename_field( + "Asset Finance Book", "daily_depreciation", "depreciation_amount_based_on_num_days_in_month" + ) + rename_field( + "Asset Depreciation Schedule", + "daily_depreciation", + "depreciation_amount_based_on_num_days_in_month", + ) + + except Exception as e: + if e.args[0] != 1054: + raise diff --git a/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py b/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py new file mode 100644 index 00000000000..2c03c23d875 --- /dev/null +++ b/erpnext/patches/v15_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.utils.rename_field import rename_field + + +def execute(): + try: + rename_field( + "Asset Finance Book", "depreciation_amount_based_on_num_days_in_month", "daily_prorata_based" + ) + rename_field( + "Asset Depreciation Schedule", + "depreciation_amount_based_on_num_days_in_month", + "daily_prorata_based", + ) + + except Exception as e: + if e.args[0] != 1054: + raise diff --git a/erpnext/patches/v15_0/set_reserved_stock_in_bin.py b/erpnext/patches/v15_0/set_reserved_stock_in_bin.py new file mode 100644 index 00000000000..fd0a23333ed --- /dev/null +++ b/erpnext/patches/v15_0/set_reserved_stock_in_bin.py @@ -0,0 +1,24 @@ +import frappe +from frappe.query_builder.functions import Sum + + +def execute(): + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select( + sre.item_code, + sre.warehouse, + Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_stock"), + ) + .where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"]))) + .groupby(sre.item_code, sre.warehouse) + ) + + for d in query.run(as_dict=True): + frappe.db.set_value( + "Bin", + {"item_code": d.item_code, "warehouse": d.warehouse}, + "reserved_stock", + d.reserved_stock, + ) diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index fd2b6a4eaa0..79fd2ebdbe9 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -3,10 +3,10 @@ frappe.ui.form.on('Quality Procedure', { refresh: function(frm) { - frm.set_query("procedure","processes", (frm) =>{ + frm.set_query('procedure', 'processes', (frm) =>{ return { filters: { - name: ["not in", [frm.parent_quality_procedure, frm.name]] + name: ['not in', [frm.parent_quality_procedure, frm.name]] } }; }); @@ -14,7 +14,8 @@ frappe.ui.form.on('Quality Procedure', { frm.set_query('parent_quality_procedure', function(){ return { filters: { - is_group: 1 + is_group: 1, + name: ['!=', frm.doc.name] } }; }); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index e8604080fbf..6834abc9d41 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -16,16 +16,13 @@ class QualityProcedure(NestedSet): def on_update(self): NestedSet.on_update(self) self.set_parent() + self.remove_parent_from_old_child() + self.add_child_to_parent() + self.remove_child_from_old_parent() def after_insert(self): self.set_parent() - - # add child to parent if missing - if self.parent_quality_procedure: - parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) - if not [d for d in parent.processes if d.procedure == self.name]: - parent.append("processes", {"procedure": self.name, "process_description": self.name}) - parent.save() + self.add_child_to_parent() def on_trash(self): # clear from child table (sub procedures) @@ -36,15 +33,6 @@ class QualityProcedure(NestedSet): ) NestedSet.on_trash(self, allow_root_deletion=True) - def set_parent(self): - for process in self.processes: - # Set parent for only those children who don't have a parent - has_parent = frappe.db.get_value( - "Quality Procedure", process.procedure, "parent_quality_procedure" - ) - if not has_parent and process.procedure: - frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) - def check_for_incorrect_child(self): for process in self.processes: if process.procedure: @@ -61,6 +49,48 @@ class QualityProcedure(NestedSet): title=_("Invalid Child Procedure"), ) + def set_parent(self): + """Set `Parent Procedure` in `Child Procedures`""" + + for process in self.processes: + if process.procedure: + if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"): + frappe.db.set_value( + "Quality Procedure", process.procedure, "parent_quality_procedure", self.name + ) + + def remove_parent_from_old_child(self): + """Remove `Parent Procedure` from `Old Child Procedures`""" + + if old_doc := self.get_doc_before_save(): + if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]): + current_child_procedures = set([d.procedure for d in self.processes if d.procedure]) + + if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)): + for child_procedure in removed_child_procedures: + frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None) + + def add_child_to_parent(self): + """Add `Child Procedure` to `Parent Procedure`""" + + if self.parent_quality_procedure: + parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) + if not [d for d in parent.processes if d.procedure == self.name]: + parent.append("processes", {"procedure": self.name, "process_description": self.name}) + parent.save() + + def remove_child_from_old_parent(self): + """Remove `Child Procedure` from `Old Parent Procedure`""" + + if old_doc := self.get_doc_before_save(): + if old_parent := old_doc.parent_quality_procedure: + if self.parent_quality_procedure != old_parent: + parent = frappe.get_doc("Quality Procedure", old_parent) + for process in parent.processes: + if process.procedure == self.name: + parent.remove(process) + parent.save() + @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index 04e82112142..467186debd9 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -1,56 +1,107 @@ # Copyright (c) 2018, Frappe and Contributors # See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from .quality_procedure import add_node -class TestQualityProcedure(unittest.TestCase): +class TestQualityProcedure(FrappeTestCase): def test_add_node(self): - try: - procedure = frappe.get_doc( - dict( - doctype="Quality Procedure", - quality_procedure_name="Test Procedure 1", - processes=[dict(process_description="Test Step 1")], - ) - ).insert() - - frappe.local.form_dict = frappe._dict( - doctype="Quality Procedure", - quality_procedure_name="Test Child 1", - parent_quality_procedure=procedure.name, - cmd="test", - is_root="false", - ) - node = add_node() - - procedure.reload() - - self.assertEqual(procedure.is_group, 1) - - # child row created - self.assertTrue([d for d in procedure.processes if d.procedure == node.name]) - - node.delete() - procedure.reload() - - # child unset - self.assertFalse([d for d in procedure.processes if d.name == node.name]) - - finally: - procedure.delete() - - -def create_procedure(): - return frappe.get_doc( - dict( - doctype="Quality Procedure", - quality_procedure_name="Test Procedure 1", - is_group=1, - processes=[dict(process_description="Test Step 1")], + procedure = create_procedure( + { + "quality_procedure_name": "Test Procedure 1", + "is_group": 1, + "processes": [dict(process_description="Test Step 1")], + } ) - ).insert() + + frappe.local.form_dict = frappe._dict( + doctype="Quality Procedure", + quality_procedure_name="Test Child 1", + parent_quality_procedure=procedure.name, + cmd="test", + is_root="false", + ) + node = add_node() + + procedure.reload() + + self.assertEqual(procedure.is_group, 1) + + # child row created + self.assertTrue([d for d in procedure.processes if d.procedure == node.name]) + + node.delete() + procedure.reload() + + # child unset + self.assertFalse([d for d in procedure.processes if d.name == node.name]) + + def test_remove_parent_from_old_child(self): + child_qp = create_procedure( + { + "quality_procedure_name": "Test Child 1", + "is_group": 0, + } + ) + group_qp = create_procedure( + { + "quality_procedure_name": "Test Group", + "is_group": 1, + "processes": [dict(procedure=child_qp.name)], + } + ) + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, group_qp.name) + + group_qp.reload() + del group_qp.processes[0] + group_qp.save() + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, None) + + def remove_child_from_old_parent(self): + child_qp = create_procedure( + { + "quality_procedure_name": "Test Child 1", + "is_group": 0, + } + ) + group_qp = create_procedure( + { + "quality_procedure_name": "Test Group", + "is_group": 1, + "processes": [dict(procedure=child_qp.name)], + } + ) + + group_qp.reload() + self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name]) + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, group_qp.name) + + child_qp.parent_quality_procedure = None + child_qp.save() + + group_qp.reload() + self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name]) + + +def create_procedure(kwargs=None): + kwargs = frappe._dict(kwargs or {}) + + doc = frappe.new_doc("Quality Procedure") + doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure" + doc.is_group = kwargs.is_group or 0 + + for process in kwargs.processes or []: + doc.append("processes", process) + + doc.insert() + + return doc diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 40cab9f3306..3b971231135 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -134,7 +134,7 @@ "label": "Customer Type", "oldfieldname": "customer_type", "oldfieldtype": "Select", - "options": "Company\nIndividual", + "options": "Company\nIndividual\nProprietorship\nPartnership", "reqd": 1 }, { @@ -584,7 +584,7 @@ "link_fieldname": "party" } ], - "modified": "2023-09-21 12:23:20.706020", + "modified": "2023-10-19 16:56:27.327035", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a40cde12f8f..e4f1a283167 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -217,7 +217,15 @@ class SalesOrder(SellingController): def validate_with_previous_doc(self): super(SalesOrder, self).validate_with_previous_doc( - {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} + { + "Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}, + "Quotation Item": { + "ref_dn_field": "quotation_item", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } ) if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")): diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index a11572776a2..312470d50ea 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -5,20 +5,24 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "warehouse", "item_code", - "reserved_qty", + "column_break_yreo", + "warehouse", + "section_break_stag", "actual_qty", - "ordered_qty", - "indented_qty", "planned_qty", + "indented_qty", + "ordered_qty", "projected_qty", + "column_break_xn5j", + "reserved_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", "reserved_qty_for_production_plan", - "ma_rate", + "reserved_stock", + "section_break_pmrs", "stock_uom", - "fcfs_rate", + "column_break_0slj", "valuation_rate", "stock_value" ], @@ -56,7 +60,7 @@ "fieldname": "reserved_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Reserved Quantity", + "label": "Reserved Qty", "oldfieldname": "reserved_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -67,7 +71,7 @@ "fieldtype": "Float", "in_filter": 1, "in_list_view": 1, - "label": "Actual Quantity", + "label": "Actual Qty", "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -77,7 +81,7 @@ "fieldname": "ordered_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Ordered Quantity", + "label": "Ordered Qty", "oldfieldname": "ordered_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -86,7 +90,7 @@ "default": "0.00", "fieldname": "indented_qty", "fieldtype": "Float", - "label": "Requested Quantity", + "label": "Requested Qty", "oldfieldname": "indented_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -116,20 +120,9 @@ { "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "label": "Reserved Qty for sub contract", + "label": "Reserved Qty for Subcontract", "read_only": 1 }, - { - "fieldname": "ma_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "Moving Average Rate", - "oldfieldname": "ma_rate", - "oldfieldtype": "Currency", - "print_hide": 1, - "read_only": 1, - "report_hide": 1 - }, { "fieldname": "stock_uom", "fieldtype": "Link", @@ -140,17 +133,6 @@ "options": "UOM", "read_only": 1 }, - { - "fieldname": "fcfs_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "FCFS Rate", - "oldfieldname": "fcfs_rate", - "oldfieldtype": "Currency", - "print_hide": 1, - "read_only": 1, - "report_hide": 1 - }, { "fieldname": "valuation_rate", "fieldtype": "Float", @@ -172,13 +154,40 @@ "fieldtype": "Float", "label": "Reserved Qty for Production Plan", "read_only": 1 + }, + { + "fieldname": "section_break_stag", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_yreo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_xn5j", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_pmrs", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_0slj", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "reserved_stock", + "fieldtype": "Float", + "label": "Reserved Stock", + "read_only": 1 } ], "hide_toolbar": 1, "idx": 1, "in_create": 1, "links": [], - "modified": "2023-05-02 23:26:21.806965", + "modified": "2023-11-01 16:51:17.079107", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 5abea9e69fe..8b2e5cf9ecb 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -34,10 +34,15 @@ class Bin(Document): get_reserved_qty_for_production_plan, ) - self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( + reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( self.item_code, self.warehouse ) + if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan: + return + + self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) + self.db_set( "reserved_qty_for_production_plan", flt(self.reserved_qty_for_production_plan), @@ -48,6 +53,29 @@ class Bin(Document): self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_qty_for_for_sub_assembly(self): + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_sub_assembly, + ) + + reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly( + self.item_code, self.warehouse + ) + + if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan: + return + + self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) + self.set_projected_qty() + + self.db_set( + { + "projected_qty": self.projected_qty, + "reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan), + }, + update_modified=True, + ) + def update_reserved_qty_for_production(self): """Update qty reserved for production from Production Item tables in open work orders""" @@ -148,6 +176,17 @@ class Bin(Document): self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_stock(self): + """Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry""" + + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_qty_for_item_and_warehouse, + ) + + reserved_stock = get_sre_reserved_qty_for_item_and_warehouse(self.item_code, self.warehouse) + + self.db_set("reserved_stock", flt(reserved_stock), update_modified=True) + def on_doctype_update(): frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 190575eb94e..66dd33a4000 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -365,6 +365,9 @@ class DeliveryNote(SellingController): # Update Stock Reservation Entry `Status` based on `Delivered Qty`. sre_doc.update_status() + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + qty_to_deliver -= qty_can_be_deliver if self._action == "cancel": @@ -427,6 +430,9 @@ class DeliveryNote(SellingController): # Update Stock Reservation Entry `Status` based on `Delivered Qty`. sre_doc.update_status() + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + qty_to_undelivered -= qty_can_be_undelivered def validate_against_stock_reservation_entries(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 1eecf6dc2aa..137c352e99a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1029,6 +1029,7 @@ class TestDeliveryNote(FrappeTestCase): dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3) si1 = make_sales_invoice(dn1.name) + si1.update_billed_amount_in_delivery_note = True si1.insert() si1.submit() dn1.reload() @@ -1037,6 +1038,7 @@ class TestDeliveryNote(FrappeTestCase): dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4) si2 = make_sales_invoice(dn2.name) + si2.update_billed_amount_in_delivery_note = True si2.insert() si2.submit() dn2.reload() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c31790759e3..ded1d4f042f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -562,11 +562,10 @@ class PurchaseReceipt(BuyingController): item=d, ) - elif ( - d.warehouse not in warehouse_with_no_account - or d.rejected_warehouse not in warehouse_with_no_account + elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( + d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account ): - warehouse_with_no_account.append(d.warehouse) + warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) elif ( d.item_code not in stock_items and not d.is_fixed_asset diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 1853f45f583..aa90ff03a82 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, now, nowdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -200,6 +200,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): riv.set_status("Skipped") + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_prevention_of_cancelled_transaction_riv(self): frappe.flags.dont_execute_stock_reposts = True @@ -377,6 +378,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): accounts_settings.acc_frozen_upto = "" accounts_settings.save() + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_create_repost_entry_for_cancelled_document(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 908ded50a8b..b0a75a352ff 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -161,7 +161,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage" ) ) self.queue_action("submit", timeout=2000) @@ -172,7 +172,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage" ) ) self.queue_action("cancel", timeout=2000) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index cc8a108bc97..f8675f338ed 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1471,6 +1471,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].item_name, item.item_name) self.assertEqual(se.items[0].stock_uom, item.stock_uom) + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_reposting_for_depedent_warehouse(self): from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 4817c8d8dc2..34a7cbaa72a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -674,6 +674,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sl_entry.actual_qty), 1.0) self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0) + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_backdated_stock_reco_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 7c712ce2258..68afd996b49 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -50,7 +50,7 @@ "label": "Limit timeslot for Stock Reposting" }, { - "default": "0", + "default": "1", "fieldname": "item_based_reposting", "fieldtype": "Check", "label": "Use Item based reposting" @@ -70,7 +70,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-04 16:14:29.080697", + "modified": "2023-11-01 16:14:29.080697", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 6b39965f9b0..09542826f3c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -9,6 +9,8 @@ from frappe.model.document import Document from frappe.query_builder.functions import Sum from frappe.utils import cint, flt +from erpnext.stock.utils import get_or_make_bin + class StockReservationEntry(Document): def validate(self) -> None: @@ -31,6 +33,7 @@ class StockReservationEntry(Document): self.update_reserved_qty_in_voucher() self.update_reserved_qty_in_pick_list() self.update_status() + self.update_reserved_stock_in_bin() def on_update_after_submit(self) -> None: self.can_be_updated() @@ -40,12 +43,14 @@ class StockReservationEntry(Document): self.validate_reservation_based_on_serial_and_batch() self.update_reserved_qty_in_voucher() self.update_status() + self.update_reserved_stock_in_bin() self.reload() def on_cancel(self) -> None: self.update_reserved_qty_in_voucher() self.update_reserved_qty_in_pick_list() self.update_status() + self.update_reserved_stock_in_bin() def validate_amended_doc(self) -> None: """Raises an exception if document is amended.""" @@ -341,6 +346,13 @@ class StockReservationEntry(Document): update_modified=update_modified, ) + def update_reserved_stock_in_bin(self) -> None: + """Updates `Reserved Stock` in Bin.""" + + bin_name = get_or_make_bin(self.item_code, self.warehouse) + bin_doc = frappe.get_cached_doc("Bin", bin_name) + bin_doc.update_reserved_stock() + def update_status(self, status: str = None, update_modified: bool = True) -> None: """Updates status based on Voucher Qty, Reserved Qty and Delivered Qty.""" @@ -681,6 +693,68 @@ def get_sre_reserved_qty_for_voucher_detail_no( return flt(reserved_qty[0][0]) +def get_sre_reserved_serial_nos_details( + item_code: str, warehouse: str, serial_nos: list = None +) -> dict: + """Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select(sb_entry.serial_no, sre.name) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & (sre.reserved_qty > sre.delivered_qty) + & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.reservation_based_on == "Serial and Batch") + ) + .orderby(sb_entry.creation) + ) + + if serial_nos: + query = query.where(sb_entry.serial_no.isin(serial_nos)) + + return frappe._dict(query.run()) + + +def get_sre_reserved_batch_nos_details( + item_code: str, warehouse: str, batch_nos: list = None +) -> dict: + """Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select( + sb_entry.batch_no, + Sum(sb_entry.qty - sb_entry.delivered_qty), + ) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & ((sre.reserved_qty - sre.delivered_qty) > 0) + & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.reservation_based_on == "Serial and Batch") + ) + .groupby(sb_entry.batch_no) + .orderby(sb_entry.creation) + ) + + if batch_nos: + query = query.where(sb_entry.batch_no.isin(batch_nos)) + + return frappe._dict(query.run()) + + def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]: """Returns a list of SREs for the provided voucher.""" diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index f4c74a8aacb..dd023e20802 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -286,6 +286,7 @@ class TestStockReservationEntry(FrappeTestCase): self.assertEqual(item.stock_reserved_qty, sre_details.reserved_qty) self.assertEqual(sre_details.status, "Partially Reserved") + cancel_stock_reservation_entries("Sales Order", so.name) se.cancel() # Test - 3: Stock should be fully Reserved if the Available Qty to Reserve is greater than the Un-reserved Qty. @@ -493,7 +494,7 @@ class TestStockReservationEntry(FrappeTestCase): "pick_serial_and_batch_based_on": "FIFO", }, ) - def test_stock_reservation_from_pick_list(self): + def test_stock_reservation_from_pick_list(self) -> None: items_details = create_items() create_material_receipt(items_details, self.warehouse, qty=100) @@ -575,7 +576,7 @@ class TestStockReservationEntry(FrappeTestCase): "auto_reserve_stock_for_sales_order_on_purchase": 1, }, ) - def test_stock_reservation_from_purchase_receipt(self): + def test_stock_reservation_from_purchase_receipt(self) -> None: from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.selling.doctype.sales_order.sales_order import make_material_request from erpnext.stock.doctype.material_request.material_request import make_purchase_order @@ -645,6 +646,40 @@ class TestStockReservationEntry(FrappeTestCase): # Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos. self.assertEqual(set(sb_details), set(reserved_sb_details)) + @change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + }, + ) + def test_consider_reserved_stock_while_cancelling_an_inward_transaction(self) -> None: + items_details = create_items() + se = create_material_receipt(items_details, self.warehouse, qty=100) + + item_list = [] + for item_code, properties in items_details.items(): + item_list.append( + { + "item_code": item_code, + "warehouse": self.warehouse, + "qty": randint(11, 100), + "uom": properties.stock_uom, + "rate": randint(10, 400), + } + ) + + so = make_sales_order( + item_list=item_list, + warehouse=self.warehouse, + ) + so.create_stock_reservation_entries() + + # Test - 1: ValidationError should be thrown as the inwarded stock is reserved. + self.assertRaises(frappe.ValidationError, se.cancel) + def tearDown(self) -> None: cancel_all_stock_reservation_entries() return super().tearDown() diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index f00dd3e7912..90b8d453420 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -11,6 +11,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Warehouse'), fieldtype:'Link', options:'Warehouse', + default: frappe.route_options && frappe.route_options.warehouse, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); @@ -22,6 +23,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Item'), fieldtype:'Link', options:'Item', + default: frappe.route_options && frappe.route_options.item_code, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); @@ -33,6 +35,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Item Group'), fieldtype:'Link', options:'Item Group', + default: frappe.route_options && frappe.route_options.item_group, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b950f188107..f2db5755bc7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -11,17 +11,22 @@ from frappe import _, scrub from frappe.model.meta import get_field_precision from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json +from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_batches, +) from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, + get_sre_reserved_batch_nos_details, + get_sre_reserved_serial_nos_details, ) from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, + get_stock_balance, get_valuation_method, ) from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero @@ -88,6 +93,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item") if is_stock_item: bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock")) repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_bin_qty(bin_name, args) else: @@ -114,6 +120,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou "voucher_no": args.get("voucher_no"), "sle_id": args.get("name"), "creation": args.get("creation"), + "reserved_stock": args.get("reserved_stock"), }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, @@ -506,7 +513,7 @@ class update_entries_after(object): self.new_items_found = False self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.affected_transactions: Set[Tuple[str, str]] = set() - self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse) + self.reserved_stock = flt(self.args.reserved_stock) self.data = frappe._dict() self.initialize_previous_data(self.args) @@ -1709,22 +1716,23 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) - if not args.batch_no: - return + if args.batch_no: + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if is_negative_with_precision(neg_batch_sle, is_batch=True): + message = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link("Batch", args.batch_no), + frappe.get_desk_link("Warehouse", args.warehouse), + neg_batch_sle[0]["posting_date"], + neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]), + ) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) - neg_batch_sle = get_future_sle_with_negative_batch_qty(args) - if is_negative_with_precision(neg_batch_sle, is_batch=True): - message = _( - "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." - ).format( - abs(neg_batch_sle[0]["cumulative_total"]), - frappe.get_desk_link("Batch", args.batch_no), - frappe.get_desk_link("Warehouse", args.warehouse), - neg_batch_sle[0]["posting_date"], - neg_batch_sle[0]["posting_time"], - frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]), - ) - frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) + if args.reserved_stock: + validate_reserved_stock(args) def is_negative_with_precision(neg_sle, is_batch=False): @@ -1791,6 +1799,96 @@ def get_future_sle_with_negative_batch_qty(args): ) +def validate_reserved_stock(kwargs): + if kwargs.serial_no: + serial_nos = kwargs.serial_no.split("\n") + validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos) + + elif kwargs.batch_no: + validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no]) + + elif kwargs.serial_and_batch_bundle: + sbb_entries = frappe.db.get_all( + "Serial and Batch Entry", + { + "parenttype": "Serial and Batch Bundle", + "parent": kwargs.serial_and_batch_bundle, + "docstatus": 1, + }, + ["batch_no", "serial_no"], + ) + + if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]: + validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos) + elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]: + validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos) + + # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty. + precision = cint(frappe.db.get_default("float_precision")) or 2 + balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse) + + diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision) + if diff < 0 and abs(diff) > 0.0001: + msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format( + abs(diff), + frappe.get_desk_link("Item", kwargs.item_code), + frappe.get_desk_link("Warehouse", kwargs.warehouse), + nowdate(), + nowtime(), + ) + frappe.throw(msg, title=_("Reserved Stock")) + + +def validate_reserved_serial_nos(item_code, warehouse, serial_nos): + if reserved_serial_nos_details := get_sre_reserved_serial_nos_details( + item_code, warehouse, serial_nos + ): + if common_serial_nos := list( + set(serial_nos).intersection(set(reserved_serial_nos_details.keys())) + ): + msg = _( + "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." + ) + msg += "
" + msg += _("Example: Serial No {0} reserved in {1}.").format( + frappe.bold(common_serial_nos[0]), + frappe.get_desk_link( + "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]] + ), + ) + frappe.throw(msg, title=_("Reserved Serial No.")) + + +def validate_reserved_batch_nos(item_code, warehouse, batch_nos): + if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos): + available_batches = get_available_batches( + frappe._dict( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ) + ) + available_batches_map = {row.batch_no: row.qty for row in available_batches} + precision = cint(frappe.db.get_default("float_precision")) or 2 + + for batch_no in batch_nos: + diff = flt( + available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision + ) + if diff < 0 and abs(diff) > 0.0001: + msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format( + abs(diff), + frappe.get_desk_link("Batch", batch_no), + frappe.get_desk_link("Warehouse", warehouse), + nowdate(), + nowtime(), + ) + frappe.throw(msg, title=_("Reserved Stock for Batch")) + + def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool: if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)): return True diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 5e57b317937..fcee2656445 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -192,9 +192,7 @@ def mark_retrired_transaction(log_doc, doc_name): record = 0 for d in log_doc.get("logger_data"): if d.transaction_name == doc_name and d.transaction_status == "Failed": - d.retried = 1 + frappe.db.set_value("Bulk Transaction Log Detail", d.name, "retried", 1) record = record + 1 - log_doc.save() - return record