From 4a390ae3de91f4f7c8d7e5a76eee2a804d0b59c6 Mon Sep 17 00:00:00 2001 From: DaizyModi Date: Fri, 10 Jan 2025 11:06:39 +0530 Subject: [PATCH 01/78] fix: Correct Party Bank Account mapping in `Payment Entry` (cherry picked from commit 376bdc75f4f0f162dde2b493b07d5405530fc2d3) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d714df0927b7..a2a4a5185cf0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2826,7 +2826,7 @@ def get_payment_entry( if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) - pe.set("bank_account", bank_account) + pe.set("party_bank_account", bank_account) pe.set_bank_account_data() # only Purchase Invoice can be blocked individually From ad06652ed5fd28737df459be4a152b3aff89f230 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 22 Jan 2025 13:46:37 +0530 Subject: [PATCH 02/78] fix: Do no query GLs if no PCVs are posted (cherry picked from commit f4d1a5458855d49d0d7626b553d6bf2a21ab6449) --- erpnext/patches/v14_0/update_closing_balances.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index e6544485252b..2a081329ce30 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -18,10 +18,12 @@ def execute(): frappe.db.truncate("Account Closing Balance") pcv_list = get_period_closing_vouchers() - gl_entries = get_gl_entries(pcv_list) - for _, pcvs in itertools.groupby(pcv_list, key=lambda pcv: (pcv.company, pcv.period_start_date)): - process_grouped_pcvs(list(pcvs), gl_entries) + if pcv_list: + gl_entries = get_gl_entries(pcv_list) + + for _, pcvs in itertools.groupby(pcv_list, key=lambda pcv: (pcv.company, pcv.period_start_date)): + process_grouped_pcvs(list(pcvs), gl_entries) def process_grouped_pcvs(pcvs, gl_entries): From bdc65daaddf38d24a7c6860edf4b3b9390a26e45 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:53:04 +0530 Subject: [PATCH 03/78] fix: added debounce to prevent multiple clicks (backport #45369) (#45376) fix: added debounce to prevent multiple clicks (#45369) * fix: added debounce to prevent multiple clicks * fix: linters check (cherry picked from commit 9ff3101b2dba1ffd045e300d5f1d6639fb7a5230) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- .../asset_depreciation_schedule/asset_depreciation_schedule.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js index 83b5c376ac77..3f7a2e7c7d8d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -34,6 +34,7 @@ frappe.ui.form.on("Depreciation Schedule", { asset_depr_schedule_name: frm.doc.name, date: row.schedule_date, }, + debounce: 1000, callback: function (r) { frappe.model.sync(r.message); frm.refresh(); From 46a2b7a07e5326ad5fde89d030460a5e9f2b67b0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Jan 2025 18:02:49 +0530 Subject: [PATCH 04/78] fix: precision issue causing incorrect status (cherry picked from commit 4a7586cc01d589c35c8e79906a15e3f29a5f07fb) --- erpnext/manufacturing/doctype/work_order/work_order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 515520b48105..332a86979c24 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -348,8 +348,9 @@ def get_status(self, status=None): if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" - total_qty = flt(flt(self.produced_qty) + flt(self.process_loss_qty), self.precision("qty")) - if flt(total_qty) >= flt(self.qty): + precision = frappe.get_precision("Work Order", "produced_qty") + total_qty = flt(self.produced_qty, precision) + flt(self.process_loss_qty, precision) + if flt(total_qty, precision) >= flt(self.qty, precision): status = "Completed" else: status = "Cancelled" From f8099a6847f2b08423401a9e2da35e578f178891 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:15:22 +0530 Subject: [PATCH 05/78] fix: set preferred email in Employee via backend controller (backport #45320) (#45379) fix: set preferred email in Employee via backend controller (#45320) fix: set preferred email in Employee (backend) Set "Preferred Email" for Employee via validate. Unset value when prefered_contact_email is also unset. (cherry picked from commit 4481ca83ff8d616cee416851f497af9fbaeb6b13) Co-authored-by: gavin --- erpnext/setup/doctype/employee/employee.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 2fa5531d602c..31568fe50dc4 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -41,6 +41,7 @@ def validate(self): self.validate_email() self.validate_status() self.validate_reports_to() + self.set_preferred_email() self.validate_preferred_email() if self.user_id: @@ -160,9 +161,7 @@ def validate_email(self): def set_preferred_email(self): preferred_email_field = frappe.scrub(self.prefered_contact_email) - if preferred_email_field: - preferred_email = self.get(preferred_email_field) - self.prefered_email = preferred_email + self.prefered_email = self.get(preferred_email_field) if preferred_email_field else None def validate_status(self): if self.status == "Left": From 4e367dedec0babd38072338d084beb0d14906976 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:15:51 +0530 Subject: [PATCH 06/78] fix: validate non-stock item for exchange loss/gain (backport #45306) (#45380) fix: validate non-stock item for exchange loss/gain (#45306) * fix: validate non-stock item * test: add unit test to validate non-stock item exchange difference * fix: use usd supplier (cherry picked from commit 05579959f26027daf246341540ef759c2cf16855) Co-authored-by: Rethik M <85231069+rs-rethik@users.noreply.github.com> --- .../purchase_invoice/purchase_invoice.py | 1 + .../purchase_invoice/test_purchase_invoice.py | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index dc6ee6c1469d..2b4242aa2338 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1126,6 +1126,7 @@ def make_item_gl_entries(self, gl_entries): exchange_rate_map[item.purchase_receipt] and self.conversion_rate != exchange_rate_map[item.purchase_receipt] and item.net_rate == net_rate_map[item.pr_detail] + and item.item_code in stock_items ): discrepancy_caused_by_exchange_rate_difference = ( item.qty * item.net_rate diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4d62c0d354dd..bc28edbf3965 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -372,6 +372,53 @@ def test_purchase_invoice_with_exchange_rate_difference(self): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as create_purchase_invoice, + ) + + # Creating Purchase Invoice with USD currency + pr = frappe.new_doc("Purchase Receipt") + pr.currency = "USD" + pr.company = "_Test Company with perpetual inventory" + pr.conversion_rate = (70,) + pr.supplier = "_Test Supplier USD" + pr.append( + "items", + { + "item_code": "_Test Non Stock Item", + "qty": 1, + "rate": 100, + }, + ) + pr.append( + "items", + {"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"}, + ) + pr.insert() + pr.submit() + + # Createing purchase invoice against Purchase Receipt + pi = create_purchase_invoice(pr.name) + pi.conversion_rate = 80 + pi.credit_to = "_Test Payable USD - TCP1" + pi.insert() + pi.submit() + + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account") + + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value( + "GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit" + ) + + discrepancy_caused_by_exchange_rate_diff = abs( + pi.items[1].base_net_amount - pr.items[1].base_net_amount + ) + + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() From 2403cdc4d7ed51f48452197942742020b688cc00 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:16:09 +0530 Subject: [PATCH 07/78] fix: System was allowing to save payment schedule amount less than grand total (backport #45322) (#45381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: System was allowing to save payment schedule amount less than grand total (#45322) * fix: System was allowing to save payment schedule amount less than grand_total * style: After run pre-commit (cherry picked from commit b26f0b6633829405a5cbb884ceec7b5352291850) Co-authored-by: Diógenes Souza <103958767+devdiogenes@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7074710dfee0..db51a012db6e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2413,10 +2413,15 @@ def validate_payment_schedule_amount(self): ) if ( - flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total")) + abs( + flt(total, self.precision("grand_total")) + - flt(grand_total, self.precision("grand_total")) + ) > 0.1 - or flt(base_total, self.precision("base_grand_total")) - - flt(base_grand_total, self.precision("base_grand_total")) + or abs( + flt(base_total, self.precision("base_grand_total")) + - flt(base_grand_total, self.precision("base_grand_total")) + ) > 0.1 ): frappe.throw( From 767529f0ec4d982e2eaba40de00b39234c400fda Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:17:14 +0530 Subject: [PATCH 08/78] fix: batch qty calculation (backport #45367) (#45388) fix: batch qty calculation (cherry picked from commit f07a71a882d940db3d1c1e28c011410fdb279aa3) Co-authored-by: Rohit Waghchaure --- .../accounts/doctype/pos_invoice/test_pos_invoice.py | 3 ++- erpnext/stock/serial_batch_bundle.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 1dbc630e62ec..7b6b8b505437 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -835,7 +835,8 @@ def test_pos_batch_item_qty_validation(self): { "item_code": item.name, "warehouse": pos_inv2.items[0].warehouse, - "voucher_type": "Delivery Note", + "voucher_type": "POS Invoice", + "voucher_no": pos_inv2.name, "qty": 2, "avg_rate": 300, "batches": frappe._dict({"TestBatch 01": 2}), diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 59c299de352f..85adb0348d90 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -6,6 +6,7 @@ from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today from pypika import Order +from pypika.terms import ExistsCriterion from erpnext.stock.deprecated_serial_batch import ( DeprecatedBatchNoValuation, @@ -650,6 +651,7 @@ def get_batch_no_ledgers(self) -> list[dict]: parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") + sle = frappe.qb.DocType("Stock Ledger Entry") timestamp_condition = "" if self.sle.posting_date: @@ -682,6 +684,14 @@ def get_batch_no_ledgers(self) -> list[dict]: & (parent.docstatus == 1) & (parent.is_cancelled == 0) & (parent.type_of_transaction.isin(["Inward", "Outward"])) + & ( + ExistsCriterion( + frappe.qb.from_(sle) + .select(sle.name) + .where((parent.name == sle.serial_and_batch_bundle) & (sle.is_cancelled == 0)) + ) + | (parent.voucher_type == "POS Invoice") + ) ) .groupby(child.batch_no) ) From 412e22fb4e6c0c6a61af4d568d26d50880541b5c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:25:33 +0530 Subject: [PATCH 09/78] fix: added item_group filter in item_code field in stock balance report (backport #45340) (#45389) fix: added item_group filter in item_code field in stock balance report (#45340) * fix: added item_group filter in item_code field in stock balance report * feat: added filter to not show non stock items (cherry picked from commit fe43d2054507611c02c976ee254e8af1f5026fbc) Co-authored-by: Mihir Kandoi --- erpnext/stock/report/stock_balance/stock_balance.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 1d86634fd951..0d68caa7e091 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -41,8 +41,14 @@ frappe.query_reports["Stock Balance"] = { width: "80", options: "Item", get_query: function () { + let item_group = frappe.query_report.get_filter_value("item_group"); + return { query: "erpnext.controllers.queries.item_query", + filters: { + ...(item_group && { item_group }), + is_stock_item: 1, + }, }; }, }, From 04f5a72e0847f66da120e0f81fd2aefc7eac94cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:01:12 +0530 Subject: [PATCH 10/78] perf: optimize DB calls with frappe.get_all (backport #45289) (#45391) perf: optimize DB calls with frappe.get_all (#45289) * perf: reduce multiple db queries * fix: use frappe._dict instread of extra iteration --------- Co-authored-by: Sanket322 (cherry picked from commit 2a400dd3f8d25568f95e58152f55c661138bee76) Co-authored-by: Sanket Shah <113279972+Sanket322@users.noreply.github.com> --- erpnext/controllers/selling_controller.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b704cb30791d..a9258204b398 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -714,6 +714,16 @@ def validate_for_duplicate_items(self): if self.doctype == "POS Invoice": return + items = [item.item_code for item in self.get("items")] + item_stock_map = frappe._dict( + frappe.get_all( + "Item", + filters={"item_code": ["in", items]}, + fields=["item_code", "is_stock_item"], + as_list=True, + ) + ) + for d in self.get("items"): if self.doctype == "Sales Invoice": stock_items = [ @@ -747,7 +757,7 @@ def validate_for_duplicate_items(self): frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")), get_link_to_form("Selling Settings", "Selling Settings"), ) - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: + if item_stock_map.get(d.item_code): if stock_items in check_list: frappe.throw(duplicate_items_msg) else: From b9b4f6316d5a75c764b8b02aa75dcabba8450051 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 23 Jan 2025 13:07:27 +0530 Subject: [PATCH 11/78] feat: pos configuration for print receipt on complete order (#45392) --- .../doctype/pos_profile/pos_profile.json | 9 ++++++++- .../accounts/doctype/pos_profile/pos_profile.py | 1 + .../point_of_sale/pos_past_order_summary.js | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 994b6776e3ce..22f2965b86e4 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -23,6 +23,7 @@ "hide_unavailable_items", "auto_add_item_to_cart", "validate_stock_on_save", + "print_receipt_on_order_complete", "column_break_16", "update_stock", "ignore_pricing_rule", @@ -375,6 +376,12 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "default": "0", + "fieldname": "print_receipt_on_order_complete", + "fieldtype": "Check", + "label": "Print Receipt on Order Complete" } ], "icon": "icon-cog", @@ -402,7 +409,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2022-08-10 12:57:06.241439", + "modified": "2025-01-01 11:07:03.161950", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 37197e1fb7a2..ea27116e91c6 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -47,6 +47,7 @@ class POSProfile(Document): letter_head: DF.Link | None payments: DF.Table[POSPaymentMethod] print_format: DF.Link | None + print_receipt_on_order_complete: DF.Check select_print_heading: DF.Link | None selling_price_list: DF.Link | None tax_category: DF.Link | None diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index ed6e6e02dccc..df44fdb04e85 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -1,7 +1,8 @@ erpnext.PointOfSale.PastOrderSummary = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events, pos_profile }) { this.wrapper = wrapper; this.events = events; + this.pos_profile = pos_profile; this.init_component(); } @@ -355,6 +356,8 @@ erpnext.PointOfSale.PastOrderSummary = class { const condition_btns_map = this.get_condition_btn_map(after_submission); this.add_summary_btns(condition_btns_map); + + this.print_receipt_on_order_complete(); } attach_document_info(doc) { @@ -421,4 +424,16 @@ erpnext.PointOfSale.PastOrderSummary = class { toggle_component(show) { show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } + + async print_receipt_on_order_complete() { + const res = await frappe.db.get_value( + "POS Profile", + this.pos_profile, + "print_receipt_on_order_complete" + ); + + if (res.message.print_receipt_on_order_complete) { + this.print_receipt(); + } + } }; From ef15429d98a615d745542b485ee010797a1c800c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 23 Jan 2025 13:03:37 +0530 Subject: [PATCH 12/78] fix: JobCardTimeLog' object has no attribute 'remaining_time_in_mins' (cherry picked from commit 41dda35db73deb56820e9f9a4a12535077c46b7d) --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index bc1076d1340c..a1b53fb7c4a4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -309,8 +309,8 @@ def has_overlap(self, production_capacity, time_logs): return overlap def get_time_logs(self, args, doctype, open_job_cards=None): - if get_datetime(args.from_time) >= get_datetime(args.to_time): - args.to_time = add_to_date(args.from_time, minutes=args.remaining_time_in_mins) + if args.get("remaining_time_in_mins") and get_datetime(args.from_time) >= get_datetime(args.to_time): + args.to_time = add_to_date(args.from_time, minutes=args.get("remaining_time_in_mins")) jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType(doctype) From 546da297615ab0a854cda40f1a264b1194a0af8b Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 23 Jan 2025 14:17:14 +0530 Subject: [PATCH 13/78] chore: quickbooks migrator integration removal (#45393) --- .../doctype/quickbooks_migrator/__init__.py | 0 .../quickbooks_migrator.js | 77 - .../quickbooks_migrator.json | 213 --- .../quickbooks_migrator.py | 1421 ----------------- .../test_quickbooks_migrator.py | 8 - 5 files changed, 1719 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js delete mode 100644 erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json delete mode 100644 erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py delete mode 100644 erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js deleted file mode 100644 index f9364edf5aac..000000000000 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("QuickBooks Migrator", { - connect: function (frm) { - // OAuth requires user intervention to provide application access permissionsto requested scope - // Here we open a new window and redirect user to the authorization url. - // After user grants us permission to access. We will set authorization details on this doc which will force refresh. - window.open(frm.doc.authorization_url); - }, - fetch_data: function (frm) { - frm.call("migrate"); - }, - onload: function (frm) { - frm.trigger("set_indicator"); - var domain = frappe.urllib.get_base_url(); - var redirect_url = `${domain}/api/method/erpnext.erpnext_integrations.doctype.quickbooks_migrator.quickbooks_migrator.callback`; - if (frm.doc.redirect_url != redirect_url) { - frm.set_value("redirect_url", redirect_url); - } - // Instead of changing percentage width and message of single progress bar - // Show a different porgress bar for every action after some time remove the finished progress bar - // Former approach causes the progress bar to dance back and forth. - frm.trigger("set_indicator"); - frappe.realtime.on("quickbooks_progress_update", function (data) { - frm.dashboard.show_progress(data.message, (data.count / data.total) * 100, data.message); - if (data.count == data.total) { - window.setTimeout( - function (message) { - frm.dashboard.hide_progress(message); - }, - 1500, - data.messsage - ); - } - }); - }, - refresh: function (frm) { - frm.trigger("set_indicator"); - if (!frm.doc.access_token) { - // Unset access_token signifies that we don't have enough information to connect to quickbooks api and fetch data - if (frm.doc.authorization_url) { - frm.add_custom_button(__("Connect to Quickbooks"), function () { - frm.trigger("connect"); - }); - } - } - if (frm.doc.access_token) { - // If we have access_token that means we also have refresh_token we don't need user intervention anymore - // All we need now is a Company from erpnext - frm.remove_custom_button(__("Connect to Quickbooks")); - - frm.toggle_display("company_settings", 1); - frm.set_df_property("company", "reqd", 1); - if (frm.doc.company) { - frm.add_custom_button(__("Fetch Data"), function () { - frm.trigger("fetch_data"); - }); - } - } - }, - set_indicator: function (frm) { - var indicator_map = { - "Connecting to QuickBooks": [__("Connecting to QuickBooks"), "orange"], - "Connected to QuickBooks": [__("Connected to QuickBooks"), "green"], - "In Progress": [__("In Progress"), "orange"], - Complete: [__("Complete"), "green"], - Failed: [__("Failed"), "red"], - }; - if (frm.doc.status) { - var indicator = indicator_map[frm.doc.status]; - var label = indicator[0]; - var color = indicator[1]; - frm.page.set_indicator(label, color); - } - }, -}); diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json deleted file mode 100644 index 5428177914bd..000000000000 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "beta": 1, - "creation": "2018-07-10 14:48:16.757030", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "status", - "application_settings", - "client_id", - "redirect_url", - "token_endpoint", - "application_column_break", - "client_secret", - "scope", - "api_endpoint", - "authorization_settings", - "authorization_endpoint", - "refresh_token", - "code", - "authorization_column_break", - "authorization_url", - "access_token", - "quickbooks_company_id", - "company_settings", - "company", - "default_shipping_account", - "default_warehouse", - "company_column_break", - "default_cost_center", - "undeposited_funds_account" - ], - "fields": [ - { - "fieldname": "status", - "fieldtype": "Select", - "hidden": 1, - "label": "Status", - "options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed" - }, - { - "collapsible": 1, - "collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url", - "fieldname": "application_settings", - "fieldtype": "Section Break", - "label": "Application Settings" - }, - { - "fieldname": "client_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Client ID", - "reqd": 1 - }, - { - "fieldname": "redirect_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Redirect URL", - "reqd": 1 - }, - { - "default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", - "fieldname": "token_endpoint", - "fieldtype": "Data", - "label": "Token Endpoint", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "application_column_break", - "fieldtype": "Column Break" - }, - { - "fieldname": "client_secret", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Client Secret", - "reqd": 1 - }, - { - "default": "com.intuit.quickbooks.accounting", - "fieldname": "scope", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Scope", - "read_only": 1, - "reqd": 1 - }, - { - "default": "https://quickbooks.api.intuit.com/v3", - "fieldname": "api_endpoint", - "fieldtype": "Data", - "label": "API Endpoint", - "read_only": 1, - "reqd": 1 - }, - { - "collapsible": 1, - "fieldname": "authorization_settings", - "fieldtype": "Section Break", - "label": "Authorization Settings" - }, - { - "default": "https://appcenter.intuit.com/connect/oauth2", - "fieldname": "authorization_endpoint", - "fieldtype": "Data", - "label": "Authorization Endpoint", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "refresh_token", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Refresh Token" - }, - { - "fieldname": "code", - "fieldtype": "Data", - "hidden": 1, - "label": "Code" - }, - { - "fieldname": "authorization_column_break", - "fieldtype": "Column Break" - }, - { - "fieldname": "authorization_url", - "fieldtype": "Data", - "label": "Authorization URL", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "access_token", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Access Token" - }, - { - "fieldname": "quickbooks_company_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Quickbooks Company ID" - }, - { - "fieldname": "company_settings", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Company Settings" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company" - }, - { - "fieldname": "default_shipping_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Default Shipping Account", - "options": "Account" - }, - { - "fieldname": "default_warehouse", - "fieldtype": "Link", - "hidden": 1, - "label": "Default Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "company_column_break", - "fieldtype": "Column Break" - }, - { - "fieldname": "default_cost_center", - "fieldtype": "Link", - "hidden": 1, - "label": "Default Cost Center", - "options": "Cost Center" - }, - { - "fieldname": "undeposited_funds_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Undeposited Funds Account", - "options": "Account" - } - ], - "issingle": 1, - "modified": "2019-08-07 15:26:00.653433", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "QuickBooks Migrator", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py deleted file mode 100644 index 5175cbdf6a96..000000000000 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py +++ /dev/null @@ -1,1421 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json -import traceback - -import frappe -import requests -from frappe import _ -from frappe.model.document import Document -from requests_oauthlib import OAuth2Session - -from erpnext import encode_company_abbr - - -# QuickBooks requires a redirect URL, User will be redirect to this URL -# This will be a GET request -# Request parameters will have two parameters `code` and `realmId` -# `code` is required to acquire refresh_token and access_token -# `realmId` is the QuickBooks Company ID. It is Needed to actually fetch data. -@frappe.whitelist() -def callback(*args, **kwargs): - migrator = frappe.get_doc("QuickBooks Migrator") - migrator.set_indicator("Connecting to QuickBooks") - migrator.code = kwargs.get("code") - migrator.quickbooks_company_id = kwargs.get("realmId") - migrator.save() - migrator.get_tokens() - frappe.db.commit() - migrator.set_indicator("Connected to QuickBooks") - # We need this page to automatically close afterwards - frappe.respond_as_web_page("Quickbooks Authentication", html="") - - -class QuickBooksMigrator(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - access_token: DF.SmallText | None - api_endpoint: DF.Data - authorization_endpoint: DF.Data - authorization_url: DF.Data - client_id: DF.Data - client_secret: DF.Data - code: DF.Data | None - company: DF.Link | None - default_cost_center: DF.Link | None - default_shipping_account: DF.Link | None - default_warehouse: DF.Link | None - quickbooks_company_id: DF.Data | None - redirect_url: DF.Data - refresh_token: DF.SmallText | None - scope: DF.Data - status: DF.Literal[ - "Connecting to QuickBooks", "Connected to QuickBooks", "In Progress", "Complete", "Failed" - ] - token_endpoint: DF.Data - undeposited_funds_account: DF.Link | None - # end: auto-generated types - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.oauth = OAuth2Session(client_id=self.client_id, redirect_uri=self.redirect_url, scope=self.scope) - if not self.authorization_url and self.authorization_endpoint: - self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] - - def on_update(self): - if self.company: - # We need a Cost Center corresponding to the selected erpnext Company - self.default_cost_center = frappe.db.get_value("Company", self.company, "cost_center") - company_warehouses = frappe.get_all("Warehouse", filters={"company": self.company, "is_group": 0}) - if company_warehouses: - self.default_warehouse = company_warehouses[0].name - if self.authorization_endpoint: - self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] - - @frappe.whitelist() - def migrate(self): - frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long") - - def _migrate(self): - try: - self.set_indicator("In Progress") - # Add quickbooks_id field to every document so that we can lookup by Id reference - # provided by documents in API responses. - # Also add a company field to Customer Supplier and Item - self._make_custom_fields() - - self._migrate_accounts() - - # Some Quickbooks Entities like Advance Payment, Payment aren't available firectly from API - # Sales Invoice also sometimes needs to be saved as a Journal Entry - # (When Item table is not present, This appens when Invoice is attached with a "StatementCharge" "ReimburseCharge - # Details of both of these cannot be fetched from API) - # Their GL entries need to be generated from GeneralLedger Report. - self._fetch_general_ledger() - - # QuickBooks data can have transactions that do not fall in existing fiscal years in ERPNext - self._create_fiscal_years() - - self._allow_fraction_in_unit() - - # Following entities are directly available from API - # Invoice can be an exception sometimes though (as explained above). - entities_for_normal_transform = [ - "Customer", - "Item", - "Vendor", - "Preferences", - "JournalEntry", - "Purchase", - "Deposit", - "Invoice", - "CreditMemo", - "SalesReceipt", - "RefundReceipt", - "Bill", - "VendorCredit", - "Payment", - "BillPayment", - ] - for entity in entities_for_normal_transform: - self._migrate_entries(entity) - - # Following entries are not available directly from API, Need to be regenrated from GeneralLedger Report - entities_for_gl_transform = [ - "Advance Payment", - "Tax Payment", - "Sales Tax Payment", - "Purchase Tax Payment", - "Inventory Qty Adjust", - ] - for entity in entities_for_gl_transform: - self._migrate_entries_from_gl(entity) - self.set_indicator("Complete") - except Exception as e: - self.set_indicator("Failed") - self._log_error(e) - - frappe.db.commit() - - def get_tokens(self): - token = self.oauth.fetch_token( - token_url=self.token_endpoint, client_secret=self.client_secret, code=self.code - ) - self.access_token = token["access_token"] - self.refresh_token = token["refresh_token"] - self.save() - - def _refresh_tokens(self): - token = self.oauth.refresh_token( - token_url=self.token_endpoint, - client_id=self.client_id, - refresh_token=self.refresh_token, - client_secret=self.client_secret, - code=self.code, - ) - self.access_token = token["access_token"] - self.refresh_token = token["refresh_token"] - self.save() - - def _make_custom_fields(self): - doctypes_for_quickbooks_id_field = [ - "Account", - "Customer", - "Address", - "Item", - "Supplier", - "Sales Invoice", - "Journal Entry", - "Purchase Invoice", - ] - for doctype in doctypes_for_quickbooks_id_field: - self._make_custom_quickbooks_id_field(doctype) - - doctypes_for_company_field = ["Customer", "Item", "Supplier"] - for doctype in doctypes_for_company_field: - self._make_custom_company_field(doctype) - - frappe.db.commit() - - def _make_custom_quickbooks_id_field(self, doctype): - if not frappe.get_meta(doctype).has_field("quickbooks_id"): - frappe.get_doc( - { - "doctype": "Custom Field", - "label": "QuickBooks ID", - "dt": doctype, - "fieldname": "quickbooks_id", - "fieldtype": "Data", - } - ).insert() - - def _make_custom_company_field(self, doctype): - if not frappe.get_meta(doctype).has_field("company"): - frappe.get_doc( - { - "doctype": "Custom Field", - "label": "Company", - "dt": doctype, - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - } - ).insert() - - def _migrate_accounts(self): - self._make_root_accounts() - for entity in ["Account", "TaxRate", "TaxCode"]: - self._migrate_entries(entity) - - def _make_root_accounts(self): - roots = ["Asset", "Equity", "Expense", "Liability", "Income"] - for root in roots: - try: - if not frappe.db.exists( - { - "doctype": "Account", - "name": encode_company_abbr(f"{root} - QB", self.company), - "company": self.company, - } - ): - frappe.get_doc( - { - "doctype": "Account", - "account_name": f"{root} - QB", - "root_type": root, - "is_group": "1", - "company": self.company, - } - ).insert(ignore_mandatory=True) - except Exception as e: - self._log_error(e, root) - frappe.db.commit() - - def _migrate_entries(self, entity): - try: - query_uri = f"{self.api_endpoint}/company/{self.quickbooks_company_id}/query" - max_result_count = 1000 - # Count number of entries - response = self._get(query_uri, params={"query": f"""SELECT COUNT(*) FROM {entity}"""}) - entry_count = response.json()["QueryResponse"]["totalCount"] - - # fetch pages and accumulate - entries = [] - for start_position in range(1, entry_count + 1, max_result_count): - response = self._get( - query_uri, - params={ - "query": """SELECT * FROM {} STARTPOSITION {} MAXRESULTS {}""".format( - entity, start_position, max_result_count - ) - }, - ) - entries.extend(response.json()["QueryResponse"][entity]) - entries = self._preprocess_entries(entity, entries) - self._save_entries(entity, entries) - except Exception as e: - self._log_error(e, response.text) - - def _fetch_general_ledger(self): - try: - query_uri = f"{self.api_endpoint}/company/{self.quickbooks_company_id}/reports/GeneralLedger" - response = self._get( - query_uri, - params={ - "columns": ",".join(["tx_date", "txn_type", "credit_amt", "debt_amt"]), - "date_macro": "All", - "minorversion": 3, - }, - ) - self.gl_entries = {} - for section in response.json()["Rows"]["Row"]: - if section["type"] == "Section": - self._get_gl_entries_from_section(section) - self.general_ledger = {} - for account in self.gl_entries.values(): - for line in account: - type_dict = self.general_ledger.setdefault(line["type"], {}) - if line["id"] not in type_dict: - type_dict[line["id"]] = { - "id": line["id"], - "date": line["date"], - "lines": [], - } - type_dict[line["id"]]["lines"].append(line) - except Exception as e: - self._log_error(e, response.text) - - def _create_fiscal_years(self): - try: - # Assumes that exactly one fiscal year has been created so far - # Creates fiscal years till oldest ledger entry date is covered - from itertools import chain - - from frappe.utils.data import add_years, getdate - - smallest_ledger_entry_date = getdate( - min(entry["date"] for entry in chain(*self.gl_entries.values()) if entry["date"]) - ) - oldest_fiscal_year = frappe.get_all( - "Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date" - )[0] - # Keep on creating fiscal years - # until smallest_ledger_entry_date is no longer smaller than the oldest fiscal year's start date - while smallest_ledger_entry_date < oldest_fiscal_year.year_start_date: - new_fiscal_year = frappe.get_doc({"doctype": "Fiscal Year"}) - new_fiscal_year.year_start_date = add_years(oldest_fiscal_year.year_start_date, -1) - new_fiscal_year.year_end_date = add_years(oldest_fiscal_year.year_end_date, -1) - if new_fiscal_year.year_start_date.year == new_fiscal_year.year_end_date.year: - new_fiscal_year.year = new_fiscal_year.year_start_date.year - else: - new_fiscal_year.year = "{}-{}".format( - new_fiscal_year.year_start_date.year, new_fiscal_year.year_end_date.year - ) - new_fiscal_year.save() - oldest_fiscal_year = new_fiscal_year - - frappe.db.commit() - except Exception as e: - self._log_error(e) - - def _migrate_entries_from_gl(self, entity): - if entity in self.general_ledger: - self._save_entries(entity, self.general_ledger[entity].values()) - - def _save_entries(self, entity, entries): - entity_method_map = { - "Account": self._save_account, - "TaxRate": self._save_tax_rate, - "TaxCode": self._save_tax_code, - "Preferences": self._save_preference, - "Customer": self._save_customer, - "Item": self._save_item, - "Vendor": self._save_vendor, - "Invoice": self._save_invoice, - "CreditMemo": self._save_credit_memo, - "SalesReceipt": self._save_sales_receipt, - "RefundReceipt": self._save_refund_receipt, - "JournalEntry": self._save_journal_entry, - "Bill": self._save_bill, - "VendorCredit": self._save_vendor_credit, - "Payment": self._save_payment, - "BillPayment": self._save_bill_payment, - "Purchase": self._save_purchase, - "Deposit": self._save_deposit, - "Advance Payment": self._save_advance_payment, - "Tax Payment": self._save_tax_payment, - "Sales Tax Payment": self._save_tax_payment, - "Purchase Tax Payment": self._save_tax_payment, - "Inventory Qty Adjust": self._save_inventory_qty_adjust, - } - total = len(entries) - for index, entry in enumerate(entries, start=1): - self._publish( - { - "event": "progress", - "message": _("Saving {0}").format(entity), - "count": index, - "total": total, - } - ) - entity_method_map[entity](entry) - frappe.db.commit() - - def _preprocess_entries(self, entity, entries): - entity_method_map = { - "Account": self._preprocess_accounts, - "TaxRate": self._preprocess_tax_rates, - "TaxCode": self._preprocess_tax_codes, - } - preprocessor = entity_method_map.get(entity) - if preprocessor: - entries = preprocessor(entries) - return entries - - def _get_gl_entries_from_section(self, section, account=None): - if "Header" in section: - if "id" in section["Header"]["ColData"][0]: - account = self._get_account_name_by_id(section["Header"]["ColData"][0]["id"]) - elif "value" in section["Header"]["ColData"][0] and section["Header"]["ColData"][0]["value"]: - # For some reason during migrating UK company, account id is not available. - # preprocess_accounts retains name:account mapping in self.accounts - # This mapping can then be used to obtain quickbooks_id for correspondong account - # Rest is trivial - - # Some Lines in General Leder Report are shown under Not Specified - # These should be skipped - if section["Header"]["ColData"][0]["value"] == "Not Specified": - return - account_id = self.accounts[section["Header"]["ColData"][0]["value"]]["Id"] - account = self._get_account_name_by_id(account_id) - entries = [] - for row in section["Rows"]["Row"]: - if row["type"] == "Data": - data = row["ColData"] - entries.append( - { - "account": account, - "date": data[0]["value"], - "type": data[1]["value"], - "id": data[1].get("id"), - "credit": frappe.utils.flt(data[2]["value"]), - "debit": frappe.utils.flt(data[3]["value"]), - } - ) - if row["type"] == "Section": - self._get_gl_entries_from_section(row, account) - self.gl_entries.setdefault(account, []).extend(entries) - - def _preprocess_accounts(self, accounts): - self.accounts = {account["Name"]: account for account in accounts} - for account in accounts: - if any(acc["SubAccount"] and acc["ParentRef"]["value"] == account["Id"] for acc in accounts): - account["is_group"] = 1 - else: - account["is_group"] = 0 - return sorted(accounts, key=lambda account: int(account["Id"])) - - def _save_account(self, account): - mapping = { - "Bank": "Asset", - "Other Current Asset": "Asset", - "Fixed Asset": "Asset", - "Other Asset": "Asset", - "Accounts Receivable": "Asset", - "Equity": "Equity", - "Expense": "Expense", - "Other Expense": "Expense", - "Cost of Goods Sold": "Expense", - "Accounts Payable": "Liability", - "Credit Card": "Liability", - "Long Term Liability": "Liability", - "Other Current Liability": "Liability", - "Income": "Income", - "Other Income": "Income", - } - # Map Quickbooks Account Types to ERPNext root_accunts and and root_type - try: - if not frappe.db.exists( - {"doctype": "Account", "quickbooks_id": account["Id"], "company": self.company} - ): - is_child = account["SubAccount"] - is_group = account["is_group"] - # Create Two Accounts for every Group Account - if is_group: - account_id = "Group - {}".format(account["Id"]) - else: - account_id = account["Id"] - - if is_child: - parent_account = self._get_account_name_by_id( - "Group - {}".format(account["ParentRef"]["value"]) - ) - else: - parent_account = encode_company_abbr( - "{} - QB".format(mapping[account["AccountType"]]), self.company - ) - - frappe.get_doc( - { - "doctype": "Account", - "quickbooks_id": account_id, - "account_name": self._get_unique_account_name(account["Name"]), - "root_type": mapping[account["AccountType"]], - "account_type": self._get_account_type(account), - "account_currency": account["CurrencyRef"]["value"], - "parent_account": parent_account, - "is_group": is_group, - "company": self.company, - } - ).insert() - - if is_group: - # Create a Leaf account corresponding to the group account - frappe.get_doc( - { - "doctype": "Account", - "quickbooks_id": account["Id"], - "account_name": self._get_unique_account_name(account["Name"]), - "root_type": mapping[account["AccountType"]], - "account_type": self._get_account_type(account), - "account_currency": account["CurrencyRef"]["value"], - "parent_account": self._get_account_name_by_id(account_id), - "is_group": 0, - "company": self.company, - } - ).insert() - if account.get("AccountSubType") == "UndepositedFunds": - self.undeposited_funds_account = self._get_account_name_by_id(account["Id"]) - self.save() - except Exception as e: - self._log_error(e, account) - - def _get_account_type(self, account): - account_subtype_mapping = {"UndepositedFunds": "Cash"} - account_type = account_subtype_mapping.get(account.get("AccountSubType")) - if account_type is None: - account_type_mapping = { - "Accounts Payable": "Payable", - "Accounts Receivable": "Receivable", - "Bank": "Bank", - "Credit Card": "Bank", - } - account_type = account_type_mapping.get(account["AccountType"]) - return account_type - - def _preprocess_tax_rates(self, tax_rates): - self.tax_rates = {tax_rate["Id"]: tax_rate for tax_rate in tax_rates} - return tax_rates - - def _save_tax_rate(self, tax_rate): - try: - if not frappe.db.exists( - { - "doctype": "Account", - "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), - "company": self.company, - } - ): - frappe.get_doc( - { - "doctype": "Account", - "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), - "account_name": "{} - QB".format(tax_rate["Name"]), - "root_type": "Liability", - "parent_account": encode_company_abbr("{} - QB".format("Liability"), self.company), - "is_group": "0", - "company": self.company, - } - ).insert() - except Exception as e: - self._log_error(e, tax_rate) - - def _preprocess_tax_codes(self, tax_codes): - self.tax_codes = {tax_code["Id"]: tax_code for tax_code in tax_codes} - return tax_codes - - def _save_tax_code(self, tax_code): - pass - - def _save_customer(self, customer): - try: - if not frappe.db.exists( - {"doctype": "Customer", "quickbooks_id": customer["Id"], "company": self.company} - ): - try: - receivable_account = frappe.get_all( - "Account", - filters={ - "account_type": "Receivable", - "account_currency": customer["CurrencyRef"]["value"], - "company": self.company, - }, - )[0]["name"] - except Exception: - receivable_account = None - erpcustomer = frappe.get_doc( - { - "doctype": "Customer", - "quickbooks_id": customer["Id"], - "customer_name": encode_company_abbr(customer["DisplayName"], self.company), - "customer_type": "Individual", - "customer_group": "Commercial", - "default_currency": customer["CurrencyRef"]["value"], - "accounts": [{"company": self.company, "account": receivable_account}], - "territory": "All Territories", - "company": self.company, - } - ).insert() - if "BillAddr" in customer: - self._create_address(erpcustomer, "Customer", customer["BillAddr"], "Billing") - if "ShipAddr" in customer: - self._create_address(erpcustomer, "Customer", customer["ShipAddr"], "Shipping") - except Exception as e: - self._log_error(e, customer) - - def _save_item(self, item): - try: - if not frappe.db.exists( - {"doctype": "Item", "quickbooks_id": item["Id"], "company": self.company} - ): - if item["Type"] in ("Service", "Inventory"): - item_dict = { - "doctype": "Item", - "quickbooks_id": item["Id"], - "item_code": encode_company_abbr(item["Name"], self.company), - "stock_uom": "Unit", - "is_stock_item": 0, - "item_group": "All Item Groups", - "company": self.company, - "item_defaults": [ - {"company": self.company, "default_warehouse": self.default_warehouse} - ], - } - if "ExpenseAccountRef" in item: - expense_account = self._get_account_name_by_id(item["ExpenseAccountRef"]["value"]) - item_dict["item_defaults"][0]["expense_account"] = expense_account - if "IncomeAccountRef" in item: - income_account = self._get_account_name_by_id(item["IncomeAccountRef"]["value"]) - item_dict["item_defaults"][0]["income_account"] = income_account - frappe.get_doc(item_dict).insert() - except Exception as e: - self._log_error(e, item) - - def _allow_fraction_in_unit(self): - frappe.db.set_value("UOM", "Unit", "must_be_whole_number", 0) - - def _save_vendor(self, vendor): - try: - if not frappe.db.exists( - {"doctype": "Supplier", "quickbooks_id": vendor["Id"], "company": self.company} - ): - erpsupplier = frappe.get_doc( - { - "doctype": "Supplier", - "quickbooks_id": vendor["Id"], - "supplier_name": encode_company_abbr(vendor["DisplayName"], self.company), - "supplier_group": "All Supplier Groups", - "company": self.company, - } - ).insert() - if "BillAddr" in vendor: - self._create_address(erpsupplier, "Supplier", vendor["BillAddr"], "Billing") - if "ShipAddr" in vendor: - self._create_address(erpsupplier, "Supplier", vendor["ShipAddr"], "Shipping") - except Exception as e: - self._log_error(e) - - def _save_preference(self, preference): - try: - if preference["SalesFormsPrefs"]["AllowShipping"]: - default_shipping_account_id = preference["SalesFormsPrefs"]["DefaultShippingAccount"] - self.default_shipping_account = self._get_account_name_by_id( - self, default_shipping_account_id - ) - self.save() - except Exception as e: - self._log_error(e, preference) - - def _save_invoice(self, invoice): - # Invoice can be Linked with Another Transactions - # If any of these transactions is a "StatementCharge" or "ReimburseCharge" then in the UI - # item list is populated from the corresponding transaction, these items are not shown in api response - # Also as of now there is no way of fetching the corresponding transaction from api - # We in order to correctly reflect account balance make an equivalent Journal Entry - quickbooks_id = "Invoice - {}".format(invoice["Id"]) - if any( - linked["TxnType"] in ("StatementCharge", "ReimburseCharge") for linked in invoice["LinkedTxn"] - ): - self._save_invoice_as_journal_entry(invoice, quickbooks_id) - else: - self._save_sales_invoice(invoice, quickbooks_id) - - def _save_credit_memo(self, credit_memo): - # Credit Memo is equivalent to a return Sales Invoice - quickbooks_id = "Credit Memo - {}".format(credit_memo["Id"]) - self._save_sales_invoice(credit_memo, quickbooks_id, is_return=True) - - def _save_sales_receipt(self, sales_receipt): - # Sales Receipt is equivalent to a POS Sales Invoice - quickbooks_id = "Sales Receipt - {}".format(sales_receipt["Id"]) - self._save_sales_invoice(sales_receipt, quickbooks_id, is_pos=True) - - def _save_refund_receipt(self, refund_receipt): - # Refund Receipt is equivalent to a return POS Sales Invoice - quickbooks_id = "Refund Receipt - {}".format(refund_receipt["Id"]) - self._save_sales_invoice(refund_receipt, quickbooks_id, is_return=True, is_pos=True) - - def _save_sales_invoice(self, invoice, quickbooks_id, is_return=False, is_pos=False): - try: - if not frappe.db.exists( - {"doctype": "Sales Invoice", "quickbooks_id": quickbooks_id, "company": self.company} - ): - invoice_dict = { - "doctype": "Sales Invoice", - "quickbooks_id": quickbooks_id, - # Quickbooks uses ISO 4217 Code - # of course this gonna come back to bite me - "currency": invoice["CurrencyRef"]["value"], - # Exchange Rate is provided if multicurrency is enabled - # It is not provided if multicurrency is not enabled - "conversion_rate": invoice.get("ExchangeRate", 1), - "posting_date": invoice["TxnDate"], - # QuickBooks doesn't make Due Date a mandatory field this is a hack - "due_date": invoice.get("DueDate", invoice["TxnDate"]), - "customer": frappe.get_all( - "Customer", - filters={ - "quickbooks_id": invoice["CustomerRef"]["value"], - "company": self.company, - }, - )[0]["name"], - "items": self._get_si_items(invoice, is_return=is_return), - "taxes": self._get_taxes(invoice), - # Do not change posting_date upon submission - "set_posting_time": 1, - # QuickBooks doesn't round total - "disable_rounded_total": 1, - "is_return": is_return, - "is_pos": is_pos, - "payments": self._get_invoice_payments(invoice, is_return=is_return, is_pos=is_pos), - "company": self.company, - } - discount = self._get_discount(invoice["Line"]) - if discount: - if invoice["ApplyTaxAfterDiscount"]: - invoice_dict["apply_discount_on"] = "Net Total" - else: - invoice_dict["apply_discount_on"] = "Grand Total" - invoice_dict["discount_amount"] = discount["Amount"] - - invoice_doc = frappe.get_doc(invoice_dict) - invoice_doc.insert() - invoice_doc.submit() - except Exception as e: - self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())]) - - def _get_si_items(self, invoice, is_return=False): - items = [] - for line in invoice["Line"]: - if line["DetailType"] == "SalesItemLineDetail": - if line["SalesItemLineDetail"]["TaxCodeRef"]["value"] != "TAX": - tax_code = line["SalesItemLineDetail"]["TaxCodeRef"]["value"] - else: - if "TxnTaxCodeRef" in invoice["TxnTaxDetail"]: - tax_code = invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"] - else: - tax_code = "NON" - if line["SalesItemLineDetail"]["ItemRef"]["value"] != "SHIPPING_ITEM_ID": - item = frappe.db.get_all( - "Item", - filters={ - "quickbooks_id": line["SalesItemLineDetail"]["ItemRef"]["value"], - "company": self.company, - }, - fields=["name", "stock_uom"], - )[0] - items.append( - { - "item_code": item["name"], - "conversion_factor": 1, - "uom": item["stock_uom"], - "description": line.get( - "Description", line["SalesItemLineDetail"]["ItemRef"]["name"] - ), - "qty": line["SalesItemLineDetail"]["Qty"], - "price_list_rate": line["SalesItemLineDetail"]["UnitPrice"], - "cost_center": self.default_cost_center, - "warehouse": self.default_warehouse, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - } - ) - else: - items.append( - { - "item_name": "Shipping", - "conversion_factor": 1, - "expense_account": self._get_account_name_by_id( - "TaxRate - {}".format(line["SalesItemLineDetail"]["TaxCodeRef"]["value"]) - ), - "uom": "Unit", - "description": "Shipping", - "income_account": self.default_shipping_account, - "qty": 1, - "price_list_rate": line["Amount"], - "cost_center": self.default_cost_center, - "warehouse": self.default_warehouse, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - } - ) - if is_return: - items[-1]["qty"] *= -1 - elif line["DetailType"] == "DescriptionOnly": - items[-1].update( - { - "margin_type": "Percentage", - "margin_rate_or_amount": int(line["Description"].split("%")[0]), - } - ) - return items - - def _get_item_taxes(self, tax_code): - tax_rates = self.tax_rates - item_taxes = {} - if tax_code != "NON": - tax_code = self.tax_codes[tax_code] - for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"): - if rate_list_type in tax_code: - for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]: - if tax_rate_detail["TaxTypeApplicable"] == "TaxOnAmount": - tax_head = self._get_account_name_by_id( - "TaxRate - {}".format(tax_rate_detail["TaxRateRef"]["value"]) - ) - tax_rate = tax_rates[tax_rate_detail["TaxRateRef"]["value"]] - item_taxes[tax_head] = tax_rate["RateValue"] - return item_taxes - - def _get_invoice_payments(self, invoice, is_return=False, is_pos=False): - if is_pos: - amount = invoice["TotalAmt"] - if is_return: - amount = -amount - return [ - { - "mode_of_payment": "Cash", - "account": self._get_account_name_by_id(invoice["DepositToAccountRef"]["value"]), - "amount": amount, - } - ] - - def _get_discount(self, lines): - for line in lines: - if line["DetailType"] == "DiscountLineDetail" and "Amount" in line["DiscountLineDetail"]: - return line - - def _save_invoice_as_journal_entry(self, invoice, quickbooks_id): - try: - accounts = [] - for line in self.general_ledger["Invoice"][invoice["Id"]]["lines"]: - account_line = {"account": line["account"], "cost_center": self.default_cost_center} - if line["debit"]: - account_line["debit_in_account_currency"] = line["debit"] - elif line["credit"]: - account_line["credit_in_account_currency"] = line["credit"] - if frappe.db.get_value("Account", line["account"], "account_type") == "Receivable": - account_line["party_type"] = "Customer" - account_line["party"] = frappe.get_all( - "Customer", - filters={"quickbooks_id": invoice["CustomerRef"]["value"], "company": self.company}, - )[0]["name"] - - accounts.append(account_line) - - posting_date = invoice["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, [invoice, accounts]) - - def _save_journal_entry(self, journal_entry): - # JournalEntry is equivalent to a Journal Entry - - def _get_je_accounts(lines): - # Converts JounalEntry lines to accounts list - posting_type_field_mapping = { - "Credit": "credit_in_account_currency", - "Debit": "debit_in_account_currency", - } - accounts = [] - for line in lines: - if line["DetailType"] == "JournalEntryLineDetail": - account_name = self._get_account_name_by_id( - line["JournalEntryLineDetail"]["AccountRef"]["value"] - ) - posting_type = line["JournalEntryLineDetail"]["PostingType"] - accounts.append( - { - "account": account_name, - posting_type_field_mapping[posting_type]: line["Amount"], - "cost_center": self.default_cost_center, - } - ) - return accounts - - quickbooks_id = "Journal Entry - {}".format(journal_entry["Id"]) - accounts = _get_je_accounts(journal_entry["Line"]) - posting_date = journal_entry["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - - def __save_journal_entry(self, quickbooks_id, accounts, posting_date): - try: - if not frappe.db.exists( - {"doctype": "Journal Entry", "quickbooks_id": quickbooks_id, "company": self.company} - ): - je = frappe.get_doc( - { - "doctype": "Journal Entry", - "quickbooks_id": quickbooks_id, - "company": self.company, - "posting_date": posting_date, - "accounts": accounts, - "multi_currency": 1, - } - ) - je.insert() - je.submit() - except Exception as e: - self._log_error(e, [accounts, json.loads(je.as_json())]) - - def _save_bill(self, bill): - # Bill is equivalent to a Purchase Invoice - quickbooks_id = "Bill - {}".format(bill["Id"]) - self.__save_purchase_invoice(bill, quickbooks_id) - - def _save_vendor_credit(self, vendor_credit): - # Vendor Credit is equivalent to a return Purchase Invoice - quickbooks_id = "Vendor Credit - {}".format(vendor_credit["Id"]) - self.__save_purchase_invoice(vendor_credit, quickbooks_id, is_return=True) - - def __save_purchase_invoice(self, invoice, quickbooks_id, is_return=False): - try: - if not frappe.db.exists( - {"doctype": "Purchase Invoice", "quickbooks_id": quickbooks_id, "company": self.company} - ): - credit_to_account = self._get_account_name_by_id(invoice["APAccountRef"]["value"]) - invoice_dict = { - "doctype": "Purchase Invoice", - "quickbooks_id": quickbooks_id, - "currency": invoice["CurrencyRef"]["value"], - "conversion_rate": invoice.get("ExchangeRate", 1), - "posting_date": invoice["TxnDate"], - "due_date": invoice.get("DueDate", invoice["TxnDate"]), - "credit_to": credit_to_account, - "supplier": frappe.get_all( - "Supplier", - filters={ - "quickbooks_id": invoice["VendorRef"]["value"], - "company": self.company, - }, - )[0]["name"], - "items": self._get_pi_items(invoice, is_return=is_return), - "taxes": self._get_taxes(invoice), - "set_posting_time": 1, - "disable_rounded_total": 1, - "is_return": is_return, - "udpate_stock": 0, - "company": self.company, - } - invoice_doc = frappe.get_doc(invoice_dict) - invoice_doc.insert() - invoice_doc.submit() - except Exception as e: - self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())]) - - def _get_pi_items(self, purchase_invoice, is_return=False): - items = [] - for line in purchase_invoice["Line"]: - if line["DetailType"] == "ItemBasedExpenseLineDetail": - if line["ItemBasedExpenseLineDetail"]["TaxCodeRef"]["value"] != "TAX": - tax_code = line["ItemBasedExpenseLineDetail"]["TaxCodeRef"]["value"] - else: - if "TxnTaxCodeRef" in purchase_invoice["TxnTaxDetail"]: - tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"] - else: - tax_code = "NON" - item = frappe.db.get_all( - "Item", - filters={ - "quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], - "company": self.company, - }, - fields=["name", "stock_uom"], - )[0] - items.append( - { - "item_code": item["name"], - "conversion_factor": 1, - "uom": item["stock_uom"], - "description": line.get( - "Description", line["ItemBasedExpenseLineDetail"]["ItemRef"]["name"] - ), - "qty": line["ItemBasedExpenseLineDetail"]["Qty"], - "price_list_rate": line["ItemBasedExpenseLineDetail"]["UnitPrice"], - "warehouse": self.default_warehouse, - "cost_center": self.default_cost_center, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - } - ) - elif line["DetailType"] == "AccountBasedExpenseLineDetail": - if line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"] != "TAX": - tax_code = line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"] - else: - if "TxnTaxCodeRef" in purchase_invoice["TxnTaxDetail"]: - tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"] - else: - tax_code = "NON" - items.append( - { - "item_name": line.get( - "Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"] - ), - "conversion_factor": 1, - "expense_account": self._get_account_name_by_id( - line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"] - ), - "uom": "Unit", - "description": line.get( - "Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"] - ), - "qty": 1, - "price_list_rate": line["Amount"], - "warehouse": self.default_warehouse, - "cost_center": self.default_cost_center, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - } - ) - if is_return: - items[-1]["qty"] *= -1 - return items - - def _save_payment(self, payment): - try: - quickbooks_id = "Payment - {}".format(payment["Id"]) - # If DepositToAccountRef is not set on payment that means it actually doesn't affect any accounts - # No need to record such payment - # Such payment record is created QuickBooks Payments API - if "DepositToAccountRef" not in payment: - return - - # A Payment can be linked to multiple transactions - accounts = [] - for line in payment["Line"]: - linked_transaction = line["LinkedTxn"][0] - if linked_transaction["TxnType"] == "Invoice": - si_quickbooks_id = "Invoice - {}".format(linked_transaction["TxnId"]) - # Invoice could have been saved as a Sales Invoice or a Journal Entry - if frappe.db.exists( - { - "doctype": "Sales Invoice", - "quickbooks_id": si_quickbooks_id, - "company": self.company, - } - ): - sales_invoice = frappe.get_all( - "Sales Invoice", - filters={ - "quickbooks_id": si_quickbooks_id, - "company": self.company, - }, - fields=["name", "customer", "debit_to"], - )[0] - reference_type = "Sales Invoice" - reference_name = sales_invoice["name"] - party = sales_invoice["customer"] - party_account = sales_invoice["debit_to"] - - if frappe.db.exists( - { - "doctype": "Journal Entry", - "quickbooks_id": si_quickbooks_id, - "company": self.company, - } - ): - journal_entry = frappe.get_doc( - "Journal Entry", - { - "quickbooks_id": si_quickbooks_id, - "company": self.company, - }, - ) - # Invoice saved as a Journal Entry must have party and party_type set on line containing Receivable Account - customer_account_line = next( - filter(lambda acc: acc.party_type == "Customer", journal_entry.accounts) - ) - - reference_type = "Journal Entry" - reference_name = journal_entry.name - party = customer_account_line.party - party_account = customer_account_line.account - - accounts.append( - { - "party_type": "Customer", - "party": party, - "reference_type": reference_type, - "reference_name": reference_name, - "account": party_account, - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - - deposit_account = self._get_account_name_by_id(payment["DepositToAccountRef"]["value"]) - accounts.append( - { - "account": deposit_account, - "debit_in_account_currency": payment["TotalAmt"], - "cost_center": self.default_cost_center, - } - ) - posting_date = payment["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, [payment, accounts]) - - def _save_bill_payment(self, bill_payment): - try: - quickbooks_id = "BillPayment - {}".format(bill_payment["Id"]) - # A BillPayment can be linked to multiple transactions - accounts = [] - for line in bill_payment["Line"]: - linked_transaction = line["LinkedTxn"][0] - if linked_transaction["TxnType"] == "Bill": - pi_quickbooks_id = "Bill - {}".format(linked_transaction["TxnId"]) - if frappe.db.exists( - { - "doctype": "Purchase Invoice", - "quickbooks_id": pi_quickbooks_id, - "company": self.company, - } - ): - purchase_invoice = frappe.get_all( - "Purchase Invoice", - filters={ - "quickbooks_id": pi_quickbooks_id, - "company": self.company, - }, - fields=["name", "supplier", "credit_to"], - )[0] - reference_type = "Purchase Invoice" - reference_name = purchase_invoice["name"] - party = purchase_invoice["supplier"] - party_account = purchase_invoice["credit_to"] - accounts.append( - { - "party_type": "Supplier", - "party": party, - "reference_type": reference_type, - "reference_name": reference_name, - "account": party_account, - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - - if bill_payment["PayType"] == "Check": - bank_account_id = bill_payment["CheckPayment"]["BankAccountRef"]["value"] - elif bill_payment["PayType"] == "CreditCard": - bank_account_id = bill_payment["CreditCardPayment"]["CCAccountRef"]["value"] - - bank_account = self._get_account_name_by_id(bank_account_id) - accounts.append( - { - "account": bank_account, - "credit_in_account_currency": bill_payment["TotalAmt"], - "cost_center": self.default_cost_center, - } - ) - posting_date = bill_payment["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, [bill_payment, accounts]) - - def _save_purchase(self, purchase): - try: - quickbooks_id = "Purchase - {}".format(purchase["Id"]) - # Credit Bank Account - accounts = [ - { - "account": self._get_account_name_by_id(purchase["AccountRef"]["value"]), - "credit_in_account_currency": purchase["TotalAmt"], - "cost_center": self.default_cost_center, - } - ] - - # Debit Mentioned Accounts - for line in purchase["Line"]: - if line["DetailType"] == "AccountBasedExpenseLineDetail": - account = self._get_account_name_by_id( - line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"] - ) - elif line["DetailType"] == "ItemBasedExpenseLineDetail": - account = ( - frappe.get_doc( - "Item", - { - "quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], - "company": self.company, - }, - ) - .item_defaults[0] - .expense_account - ) - accounts.append( - { - "account": account, - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - - # Debit Tax Accounts - if "TxnTaxDetail" in purchase: - for line in purchase["TxnTaxDetail"]["TaxLine"]: - accounts.append( - { - "account": self._get_account_name_by_id( - "TaxRate - {}".format(line["TaxLineDetail"]["TaxRateRef"]["value"]) - ), - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - - # If purchase["Credit"] is set to be True then it represents a refund - if purchase.get("Credit"): - for account in accounts: - if "debit_in_account_currency" in account: - account["credit_in_account_currency"] = account["debit_in_account_currency"] - del account["debit_in_account_currency"] - else: - account["debit_in_account_currency"] = account["credit_in_account_currency"] - del account["credit_in_account_currency"] - - posting_date = purchase["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, [purchase, accounts]) - - def _save_deposit(self, deposit): - try: - quickbooks_id = "Deposit - {}".format(deposit["Id"]) - # Debit Bank Account - accounts = [ - { - "account": self._get_account_name_by_id(deposit["DepositToAccountRef"]["value"]), - "debit_in_account_currency": deposit["TotalAmt"], - "cost_center": self.default_cost_center, - } - ] - - # Credit Mentioned Accounts - for line in deposit["Line"]: - if "LinkedTxn" in line: - accounts.append( - { - "account": self.undeposited_funds_account, - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - else: - accounts.append( - { - "account": self._get_account_name_by_id( - line["DepositLineDetail"]["AccountRef"]["value"] - ), - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - } - ) - - # Debit Cashback if mentioned - if "CashBack" in deposit: - accounts.append( - { - "account": self._get_account_name_by_id(deposit["CashBack"]["AccountRef"]["value"]), - "debit_in_account_currency": deposit["CashBack"]["Amount"], - "cost_center": self.default_cost_center, - } - ) - - posting_date = deposit["TxnDate"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, [deposit, accounts]) - - def _save_advance_payment(self, advance_payment): - quickbooks_id = "Advance Payment - {}".format(advance_payment["id"]) - self.__save_ledger_entry_as_je(advance_payment, quickbooks_id) - - def _save_tax_payment(self, tax_payment): - quickbooks_id = "Tax Payment - {}".format(tax_payment["id"]) - self.__save_ledger_entry_as_je(tax_payment, quickbooks_id) - - def _save_inventory_qty_adjust(self, inventory_qty_adjust): - quickbooks_id = "Inventory Qty Adjust - {}".format(inventory_qty_adjust["id"]) - self.__save_ledger_entry_as_je(inventory_qty_adjust, quickbooks_id) - - def __save_ledger_entry_as_je(self, ledger_entry, quickbooks_id): - try: - accounts = [] - for line in ledger_entry["lines"]: - account_line = {"account": line["account"], "cost_center": self.default_cost_center} - if line["credit"]: - account_line["credit_in_account_currency"] = line["credit"] - else: - account_line["debit_in_account_currency"] = line["debit"] - accounts.append(account_line) - - posting_date = ledger_entry["date"] - self.__save_journal_entry(quickbooks_id, accounts, posting_date) - except Exception as e: - self._log_error(e, ledger_entry) - - def _get_taxes(self, entry): - taxes = [] - if "TxnTaxDetail" not in entry or "TaxLine" not in entry["TxnTaxDetail"]: - return taxes - for line in entry["TxnTaxDetail"]["TaxLine"]: - tax_rate = line["TaxLineDetail"]["TaxRateRef"]["value"] - account_head = self._get_account_name_by_id(f"TaxRate - {tax_rate}") - tax_type_applicable = self._get_tax_type(tax_rate) - if tax_type_applicable == "TaxOnAmount": - taxes.append( - { - "charge_type": "On Net Total", - "account_head": account_head, - "description": account_head, - "cost_center": self.default_cost_center, - "rate": 0, - } - ) - else: - parent_tax_rate = self._get_parent_tax_rate(tax_rate) - parent_row_id = self._get_parent_row_id(parent_tax_rate, taxes) - taxes.append( - { - "charge_type": "On Previous Row Amount", - "row_id": parent_row_id, - "account_head": account_head, - "description": account_head, - "cost_center": self.default_cost_center, - "rate": line["TaxLineDetail"]["TaxPercent"], - } - ) - return taxes - - def _get_tax_type(self, tax_rate): - for tax_code in self.tax_codes.values(): - for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"): - if rate_list_type in tax_code: - for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]: - if tax_rate_detail["TaxRateRef"]["value"] == tax_rate: - return tax_rate_detail["TaxTypeApplicable"] - - def _get_parent_tax_rate(self, tax_rate): - parent = None - for tax_code in self.tax_codes.values(): - for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"): - if rate_list_type in tax_code: - for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]: - if tax_rate_detail["TaxRateRef"]["value"] == tax_rate: - parent = tax_rate_detail["TaxOnTaxOrder"] - if parent: - for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]: - if tax_rate_detail["TaxOrder"] == parent: - return tax_rate_detail["TaxRateRef"]["value"] - - def _get_parent_row_id(self, tax_rate, taxes): - tax_account = self._get_account_name_by_id(f"TaxRate - {tax_rate}") - for index, tax in enumerate(taxes): - if tax["account_head"] == tax_account: - return index + 1 - - def _create_address(self, entity, doctype, address, address_type): - try: - if not frappe.db.exists({"doctype": "Address", "quickbooks_id": address["Id"]}): - frappe.get_doc( - { - "doctype": "Address", - "quickbooks_address_id": address["Id"], - "address_title": entity.name, - "address_type": address_type, - "address_line1": address["Line1"], - "city": address["City"], - "links": [{"link_doctype": doctype, "link_name": entity.name}], - } - ).insert() - except Exception as e: - self._log_error(e, address) - - def _get(self, *args, **kwargs): - kwargs["headers"] = { - "Accept": "application/json", - "Authorization": f"Bearer {self.access_token}", - } - response = requests.get(*args, **kwargs) - # HTTP Status code 401 here means that the access_token is expired - # We can refresh tokens and retry - # However limitless recursion does look dangerous - if response.status_code == 401: - self._refresh_tokens() - response = self._get(*args, **kwargs) - return response - - def _get_account_name_by_id(self, quickbooks_id): - return frappe.get_all("Account", filters={"quickbooks_id": quickbooks_id, "company": self.company})[ - 0 - ]["name"] - - def _publish(self, *args, **kwargs): - frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs, user=self.modified_by) - - def _get_unique_account_name(self, quickbooks_name, number=0): - if number: - quickbooks_account_name = f"{quickbooks_name} - {number} - QB" - else: - quickbooks_account_name = f"{quickbooks_name} - QB" - company_encoded_account_name = encode_company_abbr(quickbooks_account_name, self.company) - if frappe.db.exists( - {"doctype": "Account", "name": company_encoded_account_name, "company": self.company} - ): - unique_account_name = self._get_unique_account_name(quickbooks_name, number + 1) - else: - unique_account_name = quickbooks_account_name - return unique_account_name - - def _log_error(self, execption, data=""): - frappe.log_error( - title="QuickBooks Migration Error", - message="\n".join( - [ - "Data", - json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")), - "Exception", - traceback.format_exc(), - ] - ), - ) - - def set_indicator(self, status): - self.status = status - self.save() - frappe.db.commit() diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py deleted file mode 100644 index 92e79ec8a4a1..000000000000 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestQuickBooksMigrator(unittest.TestCase): - pass From 0e088dde36b2ccb2fdd17b2e2f5a76ca9f9170a1 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Sat, 28 Dec 2024 09:25:30 +0100 Subject: [PATCH 14/78] fix: postal_code_move_and_fixes (cherry picked from commit 185bbb4c206e071541d2e616fd70541c03ad79c0) --- .../crm/report/lead_details/lead_details.py | 12 +++++---- .../address_and_contacts.py | 2 +- .../web_form/addresses/addresses.json | 25 ++++++++++--------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 98dfbec18bee..608be6ec9121 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -55,12 +55,13 @@ def get_columns(): "options": "Company", "width": 120, }, - {"fieldname": "address", "label": _("Address"), "fieldtype": "Data", "width": 130}, - {"fieldname": "state", "label": _("State"), "fieldtype": "Data", "width": 100}, - {"fieldname": "pincode", "label": _("Postal Code"), "fieldtype": "Data", "width": 90}, + {"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130}, + {"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90}, + {"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100}, + {"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100}, { - "fieldname": "country", "label": _("Country"), + "fieldname": "country", "fieldtype": "Link", "options": "Country", "width": 100, @@ -93,8 +94,9 @@ def get_data(filters): lead.owner, lead.company, (Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"), - address.state, address.pincode, + address.city, + address.state, address.country, ) .where(lead.company == filters.company) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index b8ab89a4fed8..5d0e706930f0 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -31,9 +31,9 @@ def get_columns(filters): f"{frappe.unscrub(str(party_type_value))}::150", "Address Line 1", "Address Line 2", + "Postal Code", "City", "State", - "Postal Code", "Country", "Is Primary Address:Check", "First Name", diff --git a/erpnext/utilities/web_form/addresses/addresses.json b/erpnext/utilities/web_form/addresses/addresses.json index 4e2d8e36c2cb..38b739269857 100644 --- a/erpnext/utilities/web_form/addresses/addresses.json +++ b/erpnext/utilities/web_form/addresses/addresses.json @@ -84,6 +84,18 @@ "reqd": 0, "show_in_filter": 0 }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "pincode", + "fieldtype": "Data", + "hidden": 0, + "label": "Postal Code", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { "allow_read_on_all_link_options": 0, "fieldname": "city", @@ -108,18 +120,7 @@ "reqd": 0, "show_in_filter": 0 }, - { - "allow_read_on_all_link_options": 0, - "fieldname": "pincode", - "fieldtype": "Data", - "hidden": 0, - "label": "Postal Code", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0, - "show_in_filter": 0 - }, + { "allow_read_on_all_link_options": 1, "fieldname": "country", From 671f728c4a6570f226c72a62b3d8e6f1a0bc8721 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Dec 2024 13:40:03 +0530 Subject: [PATCH 15/78] refactor: configurable posting date for Exc Gain / Loss journal (cherry picked from commit 3fbd2ca0d9d9cef759669050964e2faa63af2429) --- .../doctype/accounts_settings/accounts_settings.json | 11 ++++++++++- .../doctype/accounts_settings/accounts_settings.py | 1 + .../payment_reconciliation/payment_reconciliation.py | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6250250a1709..097ae89caaf0 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -47,6 +47,7 @@ "auto_reconciliation_job_trigger", "reconciliation_queue_size", "column_break_resa", + "exchange_gain_loss_posting_date", "invoicing_settings_tab", "accounts_transactions_settings_section", "over_billing_allowance", @@ -523,6 +524,14 @@ "fieldname": "ignore_is_opening_check_for_reporting", "fieldtype": "Check", "label": "Ignore Is Opening check for reporting" + }, + { + "default": "Payment", + "description": "Only applies for Normal Payments", + "fieldname": "exchange_gain_loss_posting_date", + "fieldtype": "Select", + "label": "Posting Date Inheritance for Exchange Gain / Loss", + "options": "Invoice\nPayment" } ], "icon": "icon-cog", @@ -530,7 +539,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-18 21:24:19.840745", + "modified": "2025-01-22 17:53:47.968079", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 590422c6224f..f3aca158486c 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -45,6 +45,7 @@ class AccountsSettings(Document): enable_fuzzy_matching: DF.Check enable_immutable_ledger: DF.Check enable_party_matching: DF.Check + exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment"] frozen_accounts_modifier: DF.Link | None general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index db4a4b0f268a..5577f4fffda2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -424,6 +424,9 @@ def calculate_difference_on_allocation_change(self, payment_entry, invoice, allo def allocate_entries(self, args): self.validate_entries() + exc_gain_loss_posting_date = frappe.db.get_single_value( + "Accounts Settings", "exchange_gain_loss_posting_date", cache=True + ) invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments")) default_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" @@ -450,6 +453,8 @@ def allocate_entries(self, args): res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") res.update({"gain_loss_posting_date": pay.get("posting_date")}) + if exc_gain_loss_posting_date == "Invoice": + res.update({"gain_loss_posting_date": inv.get("invoice_date")}) if pay.get("amount") == 0: entries.append(res) From 763cc18aad71dc2fc1bf88dbc077d6f69efb8a00 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Dec 2024 13:40:03 +0530 Subject: [PATCH 16/78] refactor: configurable posting date for Exc Gain / Loss journal (cherry picked from commit 5257413a932f4a9eb1331d5fca6110390531fadf) --- .../accounts/doctype/accounts_settings/accounts_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 097ae89caaf0..a3e2fe82c8e4 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -390,7 +390,7 @@ { "fieldname": "section_break_jpd0", "fieldtype": "Section Break", - "label": "Payment Reconciliations" + "label": "Payment Reconciliation Settings" }, { "default": "0", @@ -568,4 +568,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From f411bcc8b56bef2d0e195dc412c56cd486ba2186 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 13:48:09 +0530 Subject: [PATCH 17/78] refactor: allow reconciliation date for exchange gain / loss (cherry picked from commit 95af63e305c51a19bfea43c7f9580c68fb93433e) --- .../doctype/accounts_settings/accounts_settings.json | 6 +++--- .../accounts/doctype/accounts_settings/accounts_settings.py | 2 +- .../payment_reconciliation/payment_reconciliation.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index a3e2fe82c8e4..3f343f610e07 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -531,7 +531,7 @@ "fieldname": "exchange_gain_loss_posting_date", "fieldtype": "Select", "label": "Posting Date Inheritance for Exchange Gain / Loss", - "options": "Invoice\nPayment" + "options": "Invoice\nPayment\nReconciliation Date" } ], "icon": "icon-cog", @@ -539,7 +539,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-22 17:53:47.968079", + "modified": "2025-01-23 13:15:44.077853", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -568,4 +568,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index f3aca158486c..31249e224550 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -45,7 +45,7 @@ class AccountsSettings(Document): enable_fuzzy_matching: DF.Check enable_immutable_ledger: DF.Check enable_party_matching: DF.Check - exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment"] + exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] frozen_accounts_modifier: DF.Link | None general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5577f4fffda2..d3d89a1c1ccc 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -455,6 +455,8 @@ def allocate_entries(self, args): res.update({"gain_loss_posting_date": pay.get("posting_date")}) if exc_gain_loss_posting_date == "Invoice": res.update({"gain_loss_posting_date": inv.get("invoice_date")}) + elif exc_gain_loss_posting_date == "Reconciliation Date": + res.update({"gain_loss_posting_date": nowdate()}) if pay.get("amount") == 0: entries.append(res) From c070a140f27ce3852b797995ab1dab7981aa6117 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 14:10:15 +0530 Subject: [PATCH 18/78] refactor: only apply configuration on normal payments patch to update default value (cherry picked from commit b2c3da135ea85fe245ec9c6066a8e68b42b64f7f) --- .../payment_reconciliation/payment_reconciliation.py | 10 ++++++---- erpnext/controllers/accounts_controller.py | 2 ++ erpnext/patches.txt | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d3d89a1c1ccc..72aa4905900a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -335,6 +335,7 @@ def add_payment_entries(self, non_reconciled_payments): for payment in non_reconciled_payments: row = self.append("payments", {}) row.update(payment) + row.is_advance = payment.book_advance_payments_in_separate_party_account def get_invoice_entries(self): # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against @@ -453,10 +454,11 @@ def allocate_entries(self, args): res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") res.update({"gain_loss_posting_date": pay.get("posting_date")}) - if exc_gain_loss_posting_date == "Invoice": - res.update({"gain_loss_posting_date": inv.get("invoice_date")}) - elif exc_gain_loss_posting_date == "Reconciliation Date": - res.update({"gain_loss_posting_date": nowdate()}) + if not pay.get("is_advance"): + if exc_gain_loss_posting_date == "Invoice": + res.update({"gain_loss_posting_date": inv.get("invoice_date")}) + elif exc_gain_loss_posting_date == "Reconciliation Date": + res.update({"gain_loss_posting_date": nowdate()}) if pay.get("amount") == 0: entries.append(res) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index db51a012db6e..b712fb0abafe 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2974,6 +2974,7 @@ def get_advance_payment_entries( (payment_ref.allocated_amount).as_("amount"), (payment_ref.name).as_("reference_row"), (payment_ref.reference_name).as_("against_order"), + (payment_entry.book_advance_payments_in_separate_party_account), ) q = q.where(payment_ref.reference_doctype == order_doctype) @@ -3018,6 +3019,7 @@ def get_common_query( (payment_entry.name).as_("reference_name"), payment_entry.posting_date, (payment_entry.remarks).as_("remarks"), + (payment_entry.book_advance_payments_in_separate_party_account), ) .where(payment_entry.payment_type == payment_type) .where(payment_entry.party_type == party_type) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f0cc713ba0be..f40801e27765 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -390,3 +390,4 @@ erpnext.patches.v15_0.update_asset_status_to_work_in_progress erpnext.patches.v15_0.rename_manufacturing_settings_field erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config +execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") From 693687d8a3ed9268774a16d7ffd8a52fa26dc9dd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 14:44:40 +0530 Subject: [PATCH 19/78] test: exc gain/loss posting date based on configuration (cherry picked from commit 2f3281579a5b4393ff336e7e2ff274d0b60c5d66) --- .../tests/test_accounts_controller.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index e557a6b2d79c..0c1049f0f0e5 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -9,6 +9,7 @@ from frappe.query_builder.functions import Sum from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, getdate, nowdate +from frappe.utils.data import getdate as convert_to_date from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry @@ -868,6 +869,69 @@ def test_16_internal_transfer_at_arms_length_price(self): self.assertEqual(pi.items[0].rate, arms_length_price) self.assertEqual(pi.items[0].valuation_rate, 100) + @IntegrationTestCase.change_settings( + "Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"} + ) + def test_17_gain_loss_posting_date_for_normal_payment(self): + # Sales Invoice in Foreign Currency + rate = 80 + rate_in_account_currency = 1 + + adv_date = convert_to_date(add_days(nowdate(), -2)) + inv_date = convert_to_date(add_days(nowdate(), -1)) + + si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=rate_in_account_currency) + + # Test payments with different exchange rates + pe = self.create_payment_entry(posting_date=adv_date, amount=1, source_exc_rate=75.1).save().submit() + + pr = self.create_payment_reconciliation() + pr.from_invoice_date = add_days(nowdate(), -1) + pr.to_invoice_date = nowdate() + pr.from_payment_date = add_days(nowdate(), -2) + pr.to_payment_date = nowdate() + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Outstanding in both currencies should be '0' + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + self.assertEqual( + getdate(nowdate()), frappe.db.get_value("Journal Entry", exc_je_for_pe[0].parent, "posting_date") + ) + # Cancel Payment + pe.reload() + pe.cancel() + + # outstanding should be same as grand total + si.reload() + self.assertEqual(si.outstanding_amount, rate_in_account_currency) + self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) From 3c3f092382b9b456fcac4ea833d2a5306877b7a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 16:09:54 +0530 Subject: [PATCH 20/78] refactor: support JE posting date in semi-auto reconciilation tool (cherry picked from commit a71718883e933c7eadc15842cae2dd59c6b1d005) # Conflicts: # erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json --- ...process_payment_reconciliation_log_allocations.json | 10 ++++++++++ .../process_payment_reconciliation_log_allocations.py | 1 + 2 files changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json index b97d73886a95..5c771c54f52b 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json @@ -20,6 +20,7 @@ "is_advance", "section_break_5", "difference_amount", + "gain_loss_posting_date", "column_break_7", "difference_account", "exchange_rate", @@ -153,11 +154,20 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Reconciled" + }, + { + "fieldname": "gain_loss_posting_date", + "fieldtype": "Date", + "label": "Difference Posting Date" } ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-03-20 21:05:43.121945", +======= + "modified": "2025-01-23 16:09:01.058574", +>>>>>>> a71718883e (refactor: support JE posting date in semi-auto reconciilation tool) "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation Log Allocations", diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py index da02e1a41e66..ca1785afdae7 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py @@ -20,6 +20,7 @@ class ProcessPaymentReconciliationLogAllocations(Document): difference_account: DF.Link | None difference_amount: DF.Currency exchange_rate: DF.Float + gain_loss_posting_date: DF.Date | None invoice_number: DF.DynamicLink invoice_type: DF.Link is_advance: DF.Data | None From 495273365be3bd3085a3384ce5e00f913711e8f4 Mon Sep 17 00:00:00 2001 From: samsul580 Date: Wed, 8 Jan 2025 14:35:32 +0600 Subject: [PATCH 21/78] feat(translations): add Bengali translations for signature and client details --- erpnext/translations/bn.csv | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index 7bfbd07be365..f77199d085c5 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -8743,3 +8743,12 @@ WhatsApp,হোয়াটসঅ্যাপ, Make a call,ফোন করুন, Approve,অনুমোদন করা, Reject,প্রত্যাখ্যান, +Signature,স্বাক্ষর, +Signature is mandatory,স্বাক্ষর আবশ্যক, +Signature is not mandatory,স্বাক্ষর আবশ্যক নয়, +Authorised Signature,অনুমোদিত স্বাক্ষর, +Billing Month,বিলিং মাস, +Name of Client,ক্লায়েন্টের নাম, +Client Name,ক্লায়েন্টের নাম, +Client,ক্লায়েন্ট, +BD,বিডি, From 10ee6f3e224626b26864e7bfadcb457ac84957bb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 16:36:55 +0530 Subject: [PATCH 22/78] chore: resolve conflict --- .../process_payment_reconciliation_log_allocations.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json index 5c771c54f52b..d7ea1c2ca688 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json @@ -163,11 +163,7 @@ ], "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-03-20 21:05:43.121945", -======= "modified": "2025-01-23 16:09:01.058574", ->>>>>>> a71718883e (refactor: support JE posting date in semi-auto reconciilation tool) "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation Log Allocations", From 3906e5c33ff53b1c85b34442d13034080ead9c93 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Jan 2025 16:41:36 +0530 Subject: [PATCH 23/78] chore: use correct decorator --- erpnext/controllers/tests/test_accounts_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 0c1049f0f0e5..c45923b2fedb 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -869,9 +869,7 @@ def test_16_internal_transfer_at_arms_length_price(self): self.assertEqual(pi.items[0].rate, arms_length_price) self.assertEqual(pi.items[0].valuation_rate, 100) - @IntegrationTestCase.change_settings( - "Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"} - ) + @change_settings("Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"}) def test_17_gain_loss_posting_date_for_normal_payment(self): # Sales Invoice in Foreign Currency rate = 80 From 86f4bf6e0166f3049a21d5b23765aab82e562cbb Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 29 Sep 2023 15:19:09 +0530 Subject: [PATCH 24/78] fix: Set right party name in bank transaction - If party name and docname are different, set the docname in Bank Transaction --- .../doctype/bank_transaction/auto_match_party.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index a1271b9c32a2..997aded5465a 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -1,4 +1,5 @@ import frappe +from frappe.core.utils import find from frappe.utils import flt from rapidfuzz import fuzz, process @@ -113,7 +114,8 @@ def match_party_name_desc_in_party(self) -> tuple | None: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name") + field = party.lower() + "_name" + names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: if not self.get(field): @@ -132,12 +134,18 @@ def match_party_name_desc_in_party(self) -> tuple | None: def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: skip = False - result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio) + result = process.extract( + query=self.get(field), + choices=[name.get("party_name") for name in names], + scorer=fuzz.token_set_ratio, + ) party_name, skip = self.process_fuzzy_result(result) if not party_name: return None, skip + # Get Party Docname from the list of dicts + party_name = find(names, lambda x: x["party_name"] == party_name).get("name") return ( party, party_name, From 153e961df75a9766512c1b9fc70c502be0df6986 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 31 Oct 2023 16:53:08 +0100 Subject: [PATCH 25/78] fix: Use `process.extract` to get the corresponding party doc name of the result - rapidfuzz accepts an iterable or a dict. dict input gives the dict key and value in the result --- .../doctype/bank_transaction/auto_match_party.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 997aded5465a..8aa870f6dce4 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -1,5 +1,4 @@ import frappe -from frappe.core.utils import find from frappe.utils import flt from rapidfuzz import fuzz, process @@ -136,7 +135,7 @@ def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: skip = False result = process.extract( query=self.get(field), - choices=[name.get("party_name") for name in names], + choices={row.get("name"): row.get("party_name") for row in names}, scorer=fuzz.token_set_ratio, ) party_name, skip = self.process_fuzzy_result(result) @@ -144,8 +143,6 @@ def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: if not party_name: return None, skip - # Get Party Docname from the list of dicts - party_name = find(names, lambda x: x["party_name"] == party_name).get("name") return ( party, party_name, @@ -158,14 +155,14 @@ def process_fuzzy_result(self, result: list | None): Returns: Result, Skip (whether or not to discontinue matching) """ - PARTY, SCORE, CUTOFF = 0, 1, 80 + SCORE, PARTY_ID, CUTOFF = 1, 2, 80 if not result or not len(result): return None, False first_result = result[0] if len(result) == 1: - return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True + return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True second_result = result[1] if first_result[SCORE] > CUTOFF: @@ -174,7 +171,7 @@ def process_fuzzy_result(self, result: list | None): if first_result[SCORE] == second_result[SCORE]: return None, True - return first_result[PARTY], True + return first_result[PARTY_ID], True else: return None, False From 60feb7cbd42090235bb3f1ad61aa86eeddcc97eb Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Jan 2025 13:21:30 +0530 Subject: [PATCH 26/78] fix: Wrong `bank_ac_no` filter + simplify convoluted logic --- .../bank_transaction/auto_match_party.py | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 8aa870f6dce4..66aab9d62ddb 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -45,45 +45,41 @@ def match(self): if not (self.bank_party_account_number or self.bank_party_iban): return None - result = self.match_account_in_party() - return result + return self.match_account_in_party() def match_account_in_party(self) -> tuple | None: - """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee""" - result = None - parties = get_parties_in_order(self.deposit) - or_filters = self.get_or_filters() + """ + Returns (Party Type, Party) if a matching account is found in Bank Account or Employee: + 1. Get party from a matching (iban/account no) Bank Account + 2. If not found, get party from Employee with matching bank account details (iban/account no) + """ + if not (self.bank_party_account_number or self.bank_party_iban): + # Nothing to match + return None - for party in parties: - party_result = frappe.db.get_all( - "Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1 - ) - - if party == "Employee" and not party_result: - # Search in Bank Accounts first for Employee, and then Employee record - if "bank_account_no" in or_filters: - or_filters["bank_ac_no"] = or_filters.pop("bank_account_no") - - party_result = frappe.db.get_all( - party, or_filters=or_filters, pluck="name", limit_page_length=1 - ) - - if "bank_ac_no" in or_filters: - or_filters["bank_account_no"] = or_filters.pop("bank_ac_no") - - if party_result: - result = ( - party, - party_result[0], - ) - break + # Search for a matching Bank Account that has party set + party_result = frappe.db.get_all( + "Bank Account", + or_filters=self.get_or_filters(), + filters={"party_type": ("is", "set"), "party": ("is", "set")}, + fields=["party", "party_type"], + limit_page_length=1, + ) + if result := party_result[0] if party_result else None: + return (result["party_type"], result["party"]) - return result + # If no party is found, search in Employee (since it has bank account details) + if employee_result := frappe.db.get_all( + "Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1 + ): + return ("Employee", employee_result[0]) - def get_or_filters(self) -> dict: + def get_or_filters(self, party: str | None = None) -> dict: + """Return OR filters for Bank Account and IBAN""" or_filters = {} if self.bank_party_account_number: - or_filters["bank_account_no"] = self.bank_party_account_number + bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no" + or_filters[bank_ac_field] = self.bank_party_account_number if self.bank_party_iban: or_filters["iban"] = self.bank_party_iban @@ -103,8 +99,7 @@ def match(self) -> tuple | None: if not (self.bank_party_name or self.description): return None - result = self.match_party_name_desc_in_party() - return result + return self.match_party_name_desc_in_party() def match_party_name_desc_in_party(self) -> tuple | None: """Fuzzy search party name and/or description against parties in the system""" @@ -113,7 +108,7 @@ def match_party_name_desc_in_party(self) -> tuple | None: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - field = party.lower() + "_name" + field = f"{party.lower()}_name" names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: @@ -140,13 +135,7 @@ def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None: ) party_name, skip = self.process_fuzzy_result(result) - if not party_name: - return None, skip - - return ( - party, - party_name, - ), skip + return ((party, party_name), skip) if party_name else (None, skip) def process_fuzzy_result(self, result: list | None): """ @@ -164,8 +153,8 @@ def process_fuzzy_result(self, result: list | None): if len(result) == 1: return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True - second_result = result[1] if first_result[SCORE] > CUTOFF: + second_result = result[1] # If multiple matches with the same score, return None but discontinue matching # Matches were found but were too close to distinguish between if first_result[SCORE] == second_result[SCORE]: @@ -177,8 +166,8 @@ def process_fuzzy_result(self, result: list | None): def get_parties_in_order(deposit: float) -> list: - parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive - if flt(deposit) > 0: - parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay - - return parties + return ( + ["Customer", "Supplier", "Employee"] # most -> least likely to pay us + if flt(deposit) > 0 + else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us + ) From 73a21c294c6917ca59232fc5671e32c5f16aaceb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:18:17 +0530 Subject: [PATCH 27/78] fix: fix creating documents from sales invoice (backport #45346) (#45408) * fix: fix creating documents from sales invoice (#45346) Co-authored-by: Meike Nedwidek (cherry picked from commit 1758e125e00ea1427d0da5e4365d4d2191477130) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.js * fix: resolved conflict --------- Co-authored-by: meike289 <63092915+meike289@users.noreply.github.com> Co-authored-by: Nabin Hait --- .../doctype/sales_invoice/sales_invoice.js | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index c07e2a392ae6..da2e362cf27d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -16,6 +16,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( setup(doc) { this.setup_posting_date_time_check(); super.setup(doc); + this.frm.make_methods = { + Dunning: this.make_dunning.bind(this), + "Invoice Discounting": this.make_invoice_discounting.bind(this), + }; } company() { super.company(); @@ -121,12 +125,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( }, __("Create") ); - - cur_frm.add_custom_button( + this.frm.add_custom_button( __("Invoice Discounting"), - function () { - cur_frm.events.create_invoice_discounting(cur_frm); - }, + this.make_invoice_discounting.bind(this), __("Create") ); @@ -135,22 +136,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( .reduce((prev, current) => prev || current, false); if (payment_is_overdue) { - this.frm.add_custom_button( - __("Dunning"), - () => { - this.frm.events.create_dunning(this.frm); - }, - __("Create") - ); + this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create")); } } if (doc.docstatus === 1) { cur_frm.add_custom_button( __("Maintenance Schedule"), - function () { - cur_frm.cscript.make_maintenance_schedule(); - }, + this.make_maintenance_schedule.bind(this), __("Create") ); } @@ -185,6 +178,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } + make_invoice_discounting() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting", + frm: this.frm, + }); + } + + make_dunning() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", + frm: this.frm, + }); + } + make_maintenance_schedule() { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule", @@ -1045,20 +1052,6 @@ frappe.ui.form.on("Sales Invoice", { frm.set_df_property("return_against", "label", __("Adjustment Against")); } }, - - create_invoice_discounting: function (frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting", - frm: frm, - }); - }, - - create_dunning: function (frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", - frm: frm, - }); - }, }); frappe.ui.form.on("Sales Invoice Timesheet", { From 5d7d3d8c19ca79480dfdf42dbe510476d0a7f821 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:18:48 +0530 Subject: [PATCH 28/78] fix: add condition to check if item is delivered by supplier in make_purchase_order_for_default_supplier() (backport #45370) (#45410) fix: add condition to check if item is delivered by supplier in make_purchase_order_for_default_supplier() (#45370) (cherry picked from commit 69464ab7ff90c127d3322e3ee7c7dfdd84689e4b) Co-authored-by: Shanuka Hewage <89955436+Shanuka-98@users.noreply.github.com> --- erpnext/selling/doctype/sales_order/sales_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 374c37f99bf4..4da1934c10ee 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1357,7 +1357,8 @@ def update_item(source, target, source_parent): "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier - and doc.item_code in items_to_map, + and doc.item_code in items_to_map + and doc.delivered_by_supplier == 1, }, }, target_doc, From fb75180a7da1795ea81ed4b73fe8b467b79aa439 Mon Sep 17 00:00:00 2001 From: Sanket Shah <113279972+Sanket322@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:42:04 +0530 Subject: [PATCH 29/78] fix: don't update party-type on change of cost center in Journal Entry (#45291) fix: don't update party-type on change of cost center Co-authored-by: Sanket322 (cherry picked from commit 19c8708e5e2ce8e717df8d65403ae17a2641ea36) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index faa38763b80d..a64aba417a6e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -430,12 +430,6 @@ frappe.ui.form.on("Journal Entry Account", { }); } }, - cost_center: function (frm, dt, dn) { - // Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0' - if (frm.doc.voucher_type != "Exchange Gain Or Loss") { - erpnext.journal_entry.set_account_details(frm, dt, dn); - } - }, account: function (frm, dt, dn) { erpnext.journal_entry.set_account_details(frm, dt, dn); From 3eb28bb0e094063405d718f1cd91ab719bd22f60 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:17:08 +0530 Subject: [PATCH 30/78] fix: set expense_account and cost_center based on company in stock entry (backport #45159) (#45416) fix: set expense_account and cost_center based on company in stock entry (#45159) * fix: set expense_account and cost_center based on company in stock entry * fix: remove is_perpetual_inventory_enabled validation for cost_center (cherry picked from commit 6ec18fb40da1b6152e43d5afa31a84bbd6090622) Co-authored-by: Sugesh G <73237300+Sugesh393@users.noreply.github.com> --- .../stock/doctype/stock_entry/stock_entry.js | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index df6a61d335bf..8441b6edd640 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -507,18 +507,6 @@ frappe.ui.form.on("Stock Entry", { }); }, - company: function (frm) { - if (frm.doc.company) { - var company_doc = frappe.get_doc(":Company", frm.doc.company); - if (company_doc.default_letter_head) { - frm.set_value("letter_head", company_doc.default_letter_head); - } - frm.trigger("toggle_display_account_head"); - - erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); - } - }, - make_retention_stock_entry: function (frm) { frappe.call({ method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse", @@ -1060,11 +1048,9 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle onload_post_render() { var me = this; - this.set_default_account(function () { - if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) { - me.frm.trigger("company"); - } - }); + if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) { + me.company(); + } this.frm.get_field("items").grid.set_multiple_add("item_code", "qty"); } @@ -1143,28 +1129,42 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.clean_up(); } - set_default_account(callback) { - var me = this; + company() { + if (this.frm.doc.company) { + var company_doc = frappe.get_doc(":Company", this.frm.doc.company); + if (company_doc.default_letter_head) { + this.frm.set_value("letter_head", company_doc.default_letter_head); + } + this.frm.trigger("toggle_display_account_head"); - if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { - return this.frm.call({ - method: "erpnext.accounts.utils.get_company_default", - args: { - fieldname: "stock_adjustment_account", - company: this.frm.doc.company, - }, - callback: function (r) { - if (!r.exc) { - $.each(me.frm.doc.items || [], function (i, d) { - if (!d.expense_account) d.expense_account = r.message; - }); - if (callback) callback(); - } - }, - }); + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) + this.set_default_account("stock_adjustment_account", "expense_account"); + this.set_default_account("cost_center", "cost_center"); + + this.frm.refresh_fields("items"); } } + set_default_account(company_fieldname, fieldname) { + var me = this; + + return this.frm.call({ + method: "erpnext.accounts.utils.get_company_default", + args: { + fieldname: company_fieldname, + company: this.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + $.each(me.frm.doc.items || [], function (i, d) { + d[fieldname] = r.message; + }); + } + }, + }); + } + clean_up() { // Clear Work Order record from locals, because it is updated via Stock Entry if ( From 6c1039316403b0095d96a64c3d53009fb358242a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:19:02 +0530 Subject: [PATCH 31/78] fix: use frappe.datetime.str_to_user (backport #45216) (#45417) fix: use frappe.datetime.str_to_user (#45216) * fix: default_datetime_format * fix: add_format_datetime * fix: update to str_to_user in point_of_sale/pos_controller.js Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> * fix: convert_to_str_to_user * fix: linters * fix: whitespace --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> (cherry picked from commit cd3f03696e4853d9e40bf45a71101552f350f9ba) Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com> --- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- erpnext/selling/page/point_of_sale/pos_item_cart.js | 4 ++-- erpnext/selling/page/point_of_sale/pos_past_order_list.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index dc7e992f654e..8f02c8d0fb28 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -155,7 +155,7 @@ erpnext.PointOfSale.Controller = class { this.page.set_title_sub( ` - Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")} + Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)} ` ); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 28cb1aef3390..9de6dbbd4292 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -988,8 +988,8 @@ erpnext.PointOfSale.ItemCart = class { .html(`${__("Last transacted")} ${__(elapsed_time)}`); res.forEach((invoice) => { - const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( - "Do MMMM, h:mma" + const posting_datetime = frappe.datetime.str_to_user( + invoice.posting_date + " " + invoice.posting_time ); let indicator_color = { Paid: "green", diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index c450d8a109a6..ab2fd3e15470 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -96,8 +96,8 @@ erpnext.PointOfSale.PastOrderList = class { } get_invoice_html(invoice) { - const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( - "Do MMMM, h:mma" + const posting_datetime = frappe.datetime.str_to_user( + invoice.posting_date + " " + invoice.posting_time ); return `
From aca8d663dd98c9ad5d377afcd9f62c8b0b324144 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:40:25 +0530 Subject: [PATCH 32/78] feat: full screen on pos (backport #45404) (#45418) feat: full screen on pos (#45404) * feat: full screen on pos * refactor: variables for label * fix: refactor and handled button label change * refactor: rename enable fullscreen label (cherry picked from commit 78c7c1c631c8f0d82acf3c680b3668396c9493df) Co-authored-by: Diptanil Saha --- .../page/point_of_sale/pos_controller.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 8f02c8d0fb28..9da530081e72 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -165,6 +165,7 @@ erpnext.PointOfSale.Controller = class { this.prepare_dom(); this.prepare_components(); this.prepare_menu(); + this.prepare_fullscreen_btn(); this.make_new_invoice(); } @@ -200,6 +201,39 @@ erpnext.PointOfSale.Controller = class { this.page.add_menu_item(__("Close the POS"), this.close_pos.bind(this), false, "Shift+Ctrl+C"); } + prepare_fullscreen_btn() { + this.page.page_actions.find(".custom-actions").empty(); + + this.page.add_button(__("Full Screen"), null, { btn_class: "btn-default fullscreen-btn" }); + + this.bind_fullscreen_events(); + } + + bind_fullscreen_events() { + this.$fullscreen_btn = this.page.page_actions.find(".fullscreen-btn"); + + this.$fullscreen_btn.on("click", function () { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + } else if (document.exitFullscreen) { + document.exitFullscreen(); + } + }); + + $(document).on("fullscreenchange", this.handle_fullscreen_change_event.bind(this)); + } + + handle_fullscreen_change_event() { + let enable_fullscreen_label = __("Full Screen"); + let exit_fullscreen_label = __("Exit Full Screen"); + + if (document.fullscreenElement) { + this.$fullscreen_btn[0].innerText = exit_fullscreen_label; + } else { + this.$fullscreen_btn[0].innerText = enable_fullscreen_label; + } + } + open_form_view() { frappe.model.sync(this.frm.doc); frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); From 224a92587d3bb74a05e5d1e66f3657b46d979f5a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:13:15 +0530 Subject: [PATCH 33/78] fix: resolved pos return setting to default mode of payment instead of user selection (backport #45377) (#45419) fix: resolved pos return setting to default mode of payment instead of user selection (#45377) * fix: resolved pos return setting to default mode of payment instead of user selection * refactor: removed console log statement * refactor: moved get_payment_data to sales_and_purchase_return.py (cherry picked from commit 54d234e05de8e28da20189a6cb50d143d12361b1) Co-authored-by: Diptanil Saha --- .../controllers/sales_and_purchase_return.py | 6 +++ .../public/js/controllers/taxes_and_totals.js | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 4d44f493b1d0..183680174298 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -1150,3 +1150,9 @@ def get_available_serial_nos(serial_nos, warehouse): return frappe.get_all( "Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name" ) + + +@frappe.whitelist() +def get_payment_data(invoice): + payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) + return payment diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index ee78e493db66..875e6980e1ee 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -822,7 +822,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } - set_total_amount_to_default_mop() { + async set_total_amount_to_default_mop() { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; @@ -844,6 +844,45 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } + /* + During returns, if an user select mode of payment other than + default mode of payment, it should retain the user selection + instead resetting it to default mode of payment. + */ + + let payment_amount = 0; + this.frm.doc.payments.forEach(payment => { + payment_amount += payment.amount + }); + + if (payment_amount == total_amount_to_pay) { + return; + } + + /* + For partial return, if the payment was made using single mode of payment + it should set the return to that mode of payment only. + */ + + let return_against_mop = await frappe.call({ + method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', + args: { + invoice: this.frm.doc.return_against + } + }); + + if (return_against_mop.message.length === 1) { + this.frm.doc.payments.forEach(payment => { + if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0; + } + }); + this.frm.refresh_fields(); + return; + } + this.frm.doc.payments.find(payment => { if (payment.default) { payment.amount = total_amount_to_pay; From dec0caeac56453a63fbc8e24f76e33e1fec56eb6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:13:57 +0530 Subject: [PATCH 34/78] fix: valuation for batch (backport #45335) (#45420) * fix: valuation for batch (cherry picked from commit 5088d8576ff0edabaa04bfd1e3b7339666ef5154) # Conflicts: # erpnext/stock/deprecated_serial_batch.py * fix: version (cherry picked from commit 8028dd2683282e60bfa74dbd445db32d748a8d24) # Conflicts: # .github/workflows/server-tests-mariadb.yml * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts --------- Co-authored-by: Rohit Waghchaure --- erpnext/stock/deprecated_serial_batch.py | 70 ++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 7611d751fdd3..38e820c4c4ff 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -245,8 +245,9 @@ def set_balance_value_from_sl_entries(self) -> None: last_sle = self.get_last_sle_for_non_batch() for d in batch_data: - self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value) - self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction) + if self.available_qty.get(d.batch_no): + self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value) + self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction) def get_last_sle_for_non_batch(self): from erpnext.stock.utils import get_combine_datetime @@ -292,6 +293,58 @@ def get_last_sle_for_non_batch(self): data = query.run(as_dict=True) return data[0] if data else {} + @deprecated + def get_last_sle_for_sabb_no_batchwise_valuation(self): + sabb = frappe.qb.DocType("Serial and Batch Bundle") + sabb_entry = frappe.qb.DocType("Serial and Batch Entry") + batch = frappe.qb.DocType("Batch") + + posting_datetime = CombineDatetime(self.sle.posting_date, self.sle.posting_time) + timestamp_condition = CombineDatetime(sabb.posting_date, sabb.posting_time) < posting_datetime + + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(sabb.posting_date, sabb.posting_time) == posting_datetime + ) & (sabb.creation < self.sle.creation) + + query = ( + frappe.qb.from_(sabb) + .inner_join(sabb_entry) + .on(sabb.name == sabb_entry.parent) + .inner_join(batch) + .on(sabb_entry.batch_no == batch.name) + .select(sabb.name) + .where( + (sabb.item_code == self.sle.item_code) + & (sabb.warehouse == self.sle.warehouse) + & (sabb_entry.batch_no.isnotnull()) + & (batch.use_batchwise_valuation == 0) + & (sabb.is_cancelled == 0) + & (sabb.docstatus == 1) + ) + .where(timestamp_condition) + .orderby(sabb.posting_date, order=Order.desc) + .orderby(sabb.posting_time, order=Order.desc) + .orderby(sabb.creation, order=Order.desc) + .limit(1) + ) + + if self.sle.voucher_detail_no: + query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no) + + data = query.run(as_dict=True) + if not data: + return {} + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": data[0].name}, + ["stock_value", "qty_after_transaction"], + as_dict=1, + ) + + return sle if sle else {} + @deprecated def set_balance_value_from_bundle(self) -> None: bundle = frappe.qb.DocType("Serial and Batch Bundle") @@ -338,7 +391,14 @@ def set_balance_value_from_bundle(self) -> None: query = query.where(bundle.voucher_type != "Pick List") - for d in query.run(as_dict=True): - self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value) - self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty) + batch_data = query.run(as_dict=True) + for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + + last_sle = self.get_last_sle_for_sabb_no_batchwise_valuation() + if not last_sle: + return + + for batch_no in self.available_qty: + self.non_batchwise_balance_value[batch_no] = flt(last_sle.stock_value) + self.non_batchwise_balance_qty[batch_no] = flt(last_sle.qty_after_transaction) From d9b342f2579fbca48070b99c9b1b91fa197a2c28 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:18:04 +0530 Subject: [PATCH 35/78] fix: remove unnecessary auth from plaid connector (backport #44305) (#45421) fix: remove unnecessary auth from plaid connector (#44305) (cherry picked from commit e82911041d49116a061bb7af9b195b5f01c2f92f) Co-authored-by: Martin Luessi --- .../doctype/plaid_settings/plaid_connector.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index f44fad333cf0..cb9f49e8c28e 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -69,23 +69,7 @@ def get_link_token(self, update_mode=False): else: return response["link_token"] - def auth(self): - try: - self.client.Auth.get(self.access_token) - except ItemError as e: - if e.code == "ITEM_LOGIN_REQUIRED": - pass - except APIError as e: - if e.code == "PLANNED_MAINTENANCE": - pass - except requests.Timeout: - pass - except Exception as e: - frappe.log_error("Plaid: Authentication error") - frappe.throw(_(str(e)), title=_("Authentication Failed")) - def get_transactions(self, start_date, end_date, account_id=None): - self.auth() kwargs = dict(access_token=self.access_token, start_date=start_date, end_date=end_date) if account_id: kwargs.update(dict(account_ids=[account_id])) From 52fdc7cecd96aa9fc74d33240fccb3cb7580b03a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:46:47 +0530 Subject: [PATCH 36/78] =?UTF-8?q?fix(material=20request):=20mapping=20Sale?= =?UTF-8?q?s=20Order=20Item=20Delivery=20Date=20to=20Mate=E2=80=A6=20(back?= =?UTF-8?q?port=20#45227)=20(#45424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(material request): mapping Sales Order Item Delivery Date to Mate… (#45227) * fix(material request): mapping Sales Order Item Delivery Date to Material Request Item Required By as mentioned in https://discuss.frappe.io/t/item-delivery-date-on-sales-order-is-not-transferred-to-material-request-item-required-by-date/140479 fixing When you create a Material Request directly on the Sales Order via → Create → Material Request, Delivery Date on Sales Order Item is not transferred to Material Request Item Required By date. * fix(linters): meaningless linters formatting message applied In order to pass the linters test which I find meaningless as it asks for the comma after the last item in a dictionary data type * fix(linters): formatting code for linters pass Linters formatting applied (cherry picked from commit 42edb9f5b17783c4a1d22297e6c3c59059bdc46e) Co-authored-by: Tufan Kaynak <31142607+toofun666@users.noreply.github.com> --- erpnext/selling/doctype/sales_order/sales_order.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 4da1934c10ee..6c92386db392 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -872,7 +872,11 @@ def update_item(source, target, source_parent): }, "Sales Order Item": { "doctype": "Material Request Item", - "field_map": {"name": "sales_order_item", "parent": "sales_order"}, + "field_map": { + "name": "sales_order_item", + "parent": "sales_order", + "delivery_date": "required_by", + }, "condition": lambda item: not frappe.db.exists( "Product Bundle", {"name": item.item_code, "disabled": 0} ) From ae5ce97fd0c18e374cdc970e58c57c3c213faa9a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:23:27 +0530 Subject: [PATCH 37/78] fix: disable load_after_mapping when purchase order created from sales order (backport #45405) (#45429) fix: disable load_after_mapping when purchase order created from sales order (#45405) (cherry picked from commit 97acbb313428697dd5d4aa73cb435cf870c92974) Co-authored-by: Venkatesh <47534423+venkat102@users.noreply.github.com> --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 6c92386db392..effc3f3894df 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1506,6 +1506,7 @@ def update_item_for_packed_item(source, target, source_parent): ) set_delivery_date(doc.items, source_name) + doc.set_onload("load_after_mapping", False) return doc From 7ff7ec792928ed3a76eda8c62adb2d121e4ff4de Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:37:36 +0530 Subject: [PATCH 38/78] fix: validate items against selling settings (backport #45288) (#45431) fix: validate items against selling settings (#45288) fix: validate_for_duplicate_items Co-authored-by: Sanket322 (cherry picked from commit d862e9b7717c75b1d337ff421a9a1ce53c71d455) Co-authored-by: Sanket Shah <113279972+Sanket322@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b712fb0abafe..8361d4d825c1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3691,6 +3691,7 @@ def validate_fg_item_for_subcontracting(new_data, is_new): ).format(frappe.bold(parent.name)) ) else: # Sales Order + parent.validate_for_duplicate_items() parent.validate_warehouse() parent.update_reserved_qty() parent.update_project() From fe5e42d2dc9a3570635bb75876ea8172862dae60 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 Jan 2025 13:53:48 +0530 Subject: [PATCH 39/78] fix: precision issue in stock entry (cherry picked from commit 9f3b8520fe06879d90838619a268c0bac76d95c5) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c95de8b48a29..a2da35c0ef25 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3244,12 +3244,13 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N } ) + precision = frappe.get_precision("Stock Entry Detail", "qty") if row.serial_nos and row.batches_to_be_consume: doc.has_serial_no = 1 doc.has_batch_no = 1 batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row) for batch_no, qty in row.batches_to_be_consume.items(): - while qty > 0: + while flt(qty, precision) > 0: qty -= 1 doc.append( "entries", @@ -3270,8 +3271,9 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N precision = frappe.get_precision("Serial and Batch Entry", "qty") doc.has_batch_no = 1 for batch_no, qty in row.batches_to_be_consume.items(): - qty = flt(qty, precision) - doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) + if flt(qty, precision) > 0: + qty = flt(qty, precision) + doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) if not doc.entries: return None From f2b946d3250120b1c00557bca54ca199389def53 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 24 Jan 2025 17:30:45 +0530 Subject: [PATCH 40/78] fix: do not check budget during reposting (#45432) (cherry picked from commit 53704b98b562aad9596f233feaefb1ec89c06aa9) --- erpnext/accounts/general_ledger.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 19da840f5436..e5e43aefa323 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -35,7 +35,7 @@ def make_gl_entries( make_acc_dimensions_offsetting_entry(gl_map) validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) - gl_map = process_gl_map(gl_map, merge_entries) + gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost) if gl_map and len(gl_map) > 1: if gl_map[0].voucher_type != "Period Closing Voucher": create_payment_ledger_entry( @@ -163,12 +163,12 @@ def validate_accounting_period(gl_map): ) -def process_gl_map(gl_map, merge_entries=True, precision=None): +def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False): if not gl_map: return [] if gl_map[0].voucher_type != "Period Closing Voucher": - gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision) + gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost) if merge_entries: gl_map = merge_similar_entries(gl_map, precision) @@ -178,13 +178,17 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): return gl_map -def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): +def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False): new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") # Validate budget against main cost center - validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) + if not from_repost: + validate_expense_against_budget( + d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) + ) + cost_center_allocation = get_cost_center_allocation_data( gl_map[0]["company"], gl_map[0]["posting_date"], cost_center ) From f9d96726f06fe246f84e4ea7647e2e0eec733240 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:18:50 +0100 Subject: [PATCH 41/78] fix: secure bulk transaction (backport #45386) (#45426) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: secure bulk transaction (#45386) --- .../purchase_invoice/purchase_invoice_list.js | 16 ++++++++----- .../sales_invoice/sales_invoice_list.js | 16 ++++++++----- .../purchase_order/purchase_order_list.js | 24 ++++++++++++------- .../supplier_quotation_list.js | 20 +++++++++++----- .../doctype/quotation/quotation_list.js | 16 ++++++++----- .../doctype/sales_order/sales_order_list.js | 24 ++++++++++++------- .../delivery_note/delivery_note_list.js | 22 ++++++++++------- erpnext/utilities/bulk_transaction.py | 3 +++ 8 files changed, 90 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 031b2341bb6a..6bfb48c13d28 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = { }, onload: function (listview) { - listview.page.add_action_item(__("Purchase Receipt"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); - }); + if (frappe.model.can_create("Purchase Receipt")) { + listview.page.add_action_item(__("Purchase Receipt"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); + }); + } - listview.page.add_action_item(__("Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 3371a63cca28..ea3ae2b6fab5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = { right_column: "grand_total", onload: function (listview) { - listview.page.add_action_item(__("Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); - }); + if (frappe.model.can_create("Delivery Note")) { + listview.page.add_action_item(__("Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); + }); + } - listview.page.add_action_item(__("Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index 7b37987b926b..3c357c0a9334 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -44,16 +44,22 @@ frappe.listview_settings["Purchase Order"] = { listview.call_for_selected_items(method, { status: "Submitted" }); }); - listview.page.add_action_item(__("Purchase Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); - }); + if (frappe.model.can_create("Purchase Invoice")) { + listview.page.add_action_item(__("Purchase Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); + }); + } - listview.page.add_action_item(__("Purchase Receipt"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); - }); + if (frappe.model.can_create("Purchase Receipt")) { + listview.page.add_action_item(__("Purchase Receipt"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); + }); + } - listview.page.add_action_item(__("Advance Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Advance Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 99fe24d8770c..1a2a514a6800 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -11,12 +11,20 @@ frappe.listview_settings["Supplier Quotation"] = { }, onload: function (listview) { - listview.page.add_action_item(__("Purchase Order"), () => { - erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); - }); + if (frappe.model.can_create("Purchase Order")) { + listview.page.add_action_item(__("Purchase Order"), () => { + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); + }); + } - listview.page.add_action_item(__("Purchase Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice"); - }); + if (frappe.model.can_create("Purchase Invoice")) { + listview.page.add_action_item(__("Purchase Invoice"), () => { + erpnext.bulk_transaction_processing.create( + listview, + "Supplier Quotation", + "Purchase Invoice" + ); + }); + } }, }; diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index ae744b9cba39..b795c3fe0bc8 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -12,13 +12,17 @@ frappe.listview_settings["Quotation"] = { }; } - listview.page.add_action_item(__("Sales Order"), () => { - erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); - }); + if (frappe.model.can_create("Sales Order")) { + listview.page.add_action_item(__("Sales Order"), () => { + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); + }); + } - listview.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); - }); + if (frappe.model.can_create("Sales Invoice")) { + listview.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); + }); + } }, get_indicator: function (doc) { diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 46d115a17135..c9bd4fc0f9d4 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -60,16 +60,22 @@ frappe.listview_settings["Sales Order"] = { listview.call_for_selected_items(method, { status: "Submitted" }); }); - listview.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); - }); + if (frappe.model.can_create("Sales Invoice")) { + listview.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); + }); + } - listview.page.add_action_item(__("Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); - }); + if (frappe.model.can_create("Delivery Note")) { + listview.page.add_action_item(__("Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); + }); + } - listview.page.add_action_item(__("Advance Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Advance Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index c6b98c4134c6..dd09f6cfcf59 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -63,16 +63,20 @@ frappe.listview_settings["Delivery Note"] = { } }; - // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); - - doclist.page.add_action_item(__("Create Delivery Trip"), action); + if (frappe.model.can_create("Delivery Trip")) { + doclist.page.add_action_item(__("Create Delivery Trip"), action); + } - doclist.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); - }); + if (frappe.model.can_create("Sales Invoice")) { + doclist.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); + }); + } - doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); - }); + if (frappe.model.can_create("Packing Slip")) { + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); + }); + } }, }; diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 7ba687941c92..51447e0591bf 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -8,6 +8,9 @@ @frappe.whitelist() def transaction_processing(data, from_doctype, to_doctype): + frappe.has_permission(from_doctype, "read", throw=True) + frappe.has_permission(to_doctype, "create", throw=True) + if isinstance(data, str): deserialized_data = json.loads(data) else: From 69c5695f6efce54776a7d2e6af11aef54ca595af Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 Jan 2025 22:42:04 +0530 Subject: [PATCH 42/78] fix: allow to fix negative stock for batch using stock reco (cherry picked from commit 2e8cde337884c4c1d7c569cad251a955cc08fce2) --- erpnext/stock/doctype/batch/batch.js | 9 ++- erpnext/stock/doctype/batch/batch.py | 2 + .../stock_reconciliation.py | 5 +- .../test_stock_reconciliation.py | 78 +++++++++++++++++++ erpnext/stock/serial_batch_bundle.py | 8 +- 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 77e4e560acfb..a485f8496392 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -54,7 +54,12 @@ frappe.ui.form.on("Batch", { frappe.call({ method: "erpnext.stock.doctype.batch.batch.get_batch_qty", - args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels }, + args: { + batch_no: frm.doc.name, + item_code: frm.doc.item, + for_stock_levels: for_stock_levels, + consider_negative_batches: 1, + }, callback: (r) => { if (!r.message) { return; @@ -71,7 +76,7 @@ frappe.ui.form.on("Batch", { // show (r.message || []).forEach(function (d) { - if (d.qty > 0) { + if (d.qty != 0) { $(`
${d.warehouse}
${d.qty}
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3882e5b24245..1e1e6c8ee26d 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -218,6 +218,7 @@ def get_batch_qty( posting_time=None, ignore_voucher_nos=None, for_stock_levels=False, + consider_negative_batches=False, ): """Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None @@ -243,6 +244,7 @@ def get_batch_qty( "batch_no": batch_no, "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, + "consider_negative_batches": consider_negative_batches, } ) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9e23270ca71d..85c74480e7dd 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -139,8 +139,8 @@ def make_bundle_for_current_qty(self): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, - "qty": row.current_qty, - "type_of_transaction": "Outward", + "qty": row.current_qty * -1, + "type_of_transaction": "Outward" if row.current_qty > 0 else "Inward", "company": self.company, "is_rejected": 0, "serial_nos": get_serial_nos(row.current_serial_no) @@ -1367,6 +1367,7 @@ def get_stock_balance_for( posting_date=posting_date, posting_time=posting_time, for_stock_levels=True, + consider_negative_batches=True, ) or 0 ) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index a3673063a48e..48a27a25962b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1330,6 +1330,84 @@ def test_skip_reposting_for_entries_after_stock_reco(self): self.assertEqual(stock_value_difference, 1500.00 * -1) + def test_stock_reco_for_negative_batch(self): + from erpnext.stock.doctype.batch.batch import get_batch_qty + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test Item For Negative Batch", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-NB-.###", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + se = make_stock_entry( + posting_date="2024-11-01", + posting_time="11:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + posting_date="2024-11-01", + posting_time="11:00", + item_code=item_code, + source=warehouse, + qty=10, + basic_rate=100, + use_serial_batch_fields=1, + batch_no=batch_no, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": se.name, "is_cancelled": 0}, + ) + + # intentionally setting negative qty + doc = frappe.get_doc("Stock Ledger Entry", sles[0].name) + doc.db_set( + { + "actual_qty": -20, + "qty_after_transaction": -10, + } + ) + + sabb_doc = frappe.get_doc("Serial and Batch Bundle", doc.serial_and_batch_bundle) + for row in sabb_doc.entries: + row.db_set("qty", -20) + + batch_qty = get_batch_qty(batch_no, warehouse, item_code, consider_negative_batches=True) + self.assertEqual(batch_qty, -10) + + sr = create_stock_reconciliation( + posting_date="2024-11-02", + posting_time="11:00", + item_code=item_code, + warehouse=warehouse, + use_serial_batch_fields=1, + batch_no=batch_no, + qty=0, + rate=100, + do_not_submit=True, + ) + + self.assertEqual(sr.items[0].current_qty, -10) + sr.submit() + sr.reload() + + self.assertTrue(sr.items[0].current_serial_and_batch_bundle) + self.assertFalse(sr.items[0].serial_and_batch_bundle) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 85adb0348d90..f4d862b583c9 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -418,7 +418,13 @@ def update_batch_qty(self): batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty}) batches_qty = get_available_batches( - frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())}) + frappe._dict( + { + "item_code": self.item_code, + "batch_no": list(batches.keys()), + "consider_negative_batches": 1, + } + ) ) for batch_no in batches: From 692a44816f698d80239d2d97cb06d073547dc045 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:14:44 +0530 Subject: [PATCH 43/78] feat(UX): scroll to required field (backport #44367) (#45433) feat(UX): scroll to required field (#44367) (cherry picked from commit 4008ca5ddd55b36b8c12277dd450ee15ba63b47f) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/public/js/queries.js | 58 +++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 46958092199f..63651ec87598 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -28,9 +28,13 @@ $.extend(erpnext.queries, { customer_filter: function (doc) { if (!doc.customer) { - frappe.throw( - __("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "customer", doc.name))]) - ); + cur_frm.scroll_to_field("customer"); + frappe.show_alert({ + message: __("Please set {0} first.", [ + __(frappe.meta.get_label(doc.doctype, "customer", doc.name)), + ]), + indicator: "orange", + }); } return { filters: { customer: doc.customer } }; @@ -39,11 +43,13 @@ $.extend(erpnext.queries, { contact_query: function (doc) { if (frappe.dynamic_link) { if (!doc[frappe.dynamic_link.fieldname]) { - frappe.throw( - __("Please set {0}", [ + cur_frm.scroll_to_field(frappe.dynamic_link.fieldname); + frappe.show_alert({ + message: __("Please set {0} first.", [ __(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)), - ]) - ); + ]), + indicator: "orange", + }); } return { @@ -70,11 +76,13 @@ $.extend(erpnext.queries, { address_query: function (doc) { if (frappe.dynamic_link) { if (!doc[frappe.dynamic_link.fieldname]) { - frappe.throw( - __("Please set {0}", [ + cur_frm.scroll_to_field(frappe.dynamic_link.fieldname); + frappe.show_alert({ + message: __("Please set {0} first.", [ __(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name)), - ]) - ); + ]), + indicator: "orange", + }); } return { @@ -89,7 +97,13 @@ $.extend(erpnext.queries, { company_address_query: function (doc) { if (!doc.company) { - frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))])); + cur_frm.scroll_to_field("company"); + frappe.show_alert({ + message: __("Please set {0} first.", [ + __(frappe.meta.get_label(doc.doctype, "company", doc.name)), + ]), + indicator: "orange", + }); } return { @@ -110,9 +124,13 @@ $.extend(erpnext.queries, { supplier_filter: function (doc) { if (!doc.supplier) { - frappe.throw( - __("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))]) - ); + cur_frm.scroll_to_field("supplier"); + frappe.show_alert({ + message: __("Please set {0} first.", [ + __(frappe.meta.get_label(doc.doctype, "supplier", doc.name)), + ]), + indicator: "orange", + }); } return { filters: { supplier: doc.supplier } }; @@ -120,9 +138,13 @@ $.extend(erpnext.queries, { lead_filter: function (doc) { if (!doc.lead) { - frappe.throw( - __("Please specify a {0}", [__(frappe.meta.get_label(doc.doctype, "lead", doc.name))]) - ); + cur_frm.scroll_to_field("lead"); + frappe.show_alert({ + message: __("Please specify a {0} first.", [ + __(frappe.meta.get_label(doc.doctype, "lead", doc.name)), + ]), + indicator: "orange", + }); } return { filters: { lead: doc.lead } }; From ff46ae5bc1af5c28aaafe9213ac6795e0d0d8892 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:21:40 +0530 Subject: [PATCH 44/78] fix: currency decimal on POS Past Order List (backport #45524) (#45527) fix: currency decimal on POS Past Order List (#45524) * fix: currency decimal on POS * fix: removed precision (cherry picked from commit 2ac8c92e7fc30a50cf6b84bfea3cc334e6fa64bd) Co-authored-by: Diptanil Saha --- erpnext/selling/page/point_of_sale/pos_past_order_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index ab2fd3e15470..dda44f252990 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -110,7 +110,7 @@ erpnext.PointOfSale.PastOrderList = class {
-
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
+
${format_currency(invoice.grand_total, invoice.currency) || 0}
${posting_datetime}
From 2c2a25ab16f5b53ddf17dc23abff6e9ab14f1e4d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:26:19 +0530 Subject: [PATCH 45/78] feat(Sales Invoice): allow linking to project without adding timesheets (backport #44295) (#45528) feat(Sales Invoice): allow linking to project without adding timesheets (#44295) * feat(Sales Invoice): allow linking to project without adding timesheets * test: add timesheet data (cherry picked from commit 11f65f20a04ef6d7fd04eb62508a75eb729758f0) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 +++++++---- erpnext/projects/doctype/timesheet/test_timesheet.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6e039b4b34fb..97871ba7d6fd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1083,13 +1083,16 @@ def set_billing_hours_and_amount(self): timesheet.billing_amount = ts_doc.total_billable_amount def update_timesheet_billing_for_project(self): - if not self.timesheets and self.project: - self.add_timesheet_data() - else: + if self.timesheets: self.calculate_billing_amount_for_timesheet() - @frappe.whitelist() + @frappe.whitelist(methods=["PUT"]) def add_timesheet_data(self): + if not self.timesheets and self.project: + self._add_timesheet_data() + self.save() + + def _add_timesheet_data(self): self.set("timesheets", []) if self.project: for data in get_projectwise_timesheet_data(self.project): diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index da042f36aef1..39140b335c93 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -62,6 +62,7 @@ def test_timesheet_billing_based_on_project(self): ) sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = project + sales_invoice._add_timesheet_data() sales_invoice.submit() ts = frappe.get_doc("Timesheet", timesheet.name) From 172fdad24457fca4c5cec4a64e125213db4ff7db Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Wed, 15 Jan 2025 18:20:20 +0530 Subject: [PATCH 46/78] fix: set party_account_currency for pos_invoice returns (cherry picked from commit 2af6fca7fa8a5230c4949017210200da12613b41) --- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 183680174298..2079338b09ad 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -370,6 +370,8 @@ def set_missing_values(source, target): if doc.get("is_return"): if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice": doc.consolidated_invoice = "" + # no copy enabled for party_account_currency + doc.party_account_currency = source.party_account_currency doc.set("payments", []) doc.update_billed_amount_in_delivery_note = True for data in source.payments: From 6a382f14301001f6042abbc1e487e035ced760bc Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Wed, 15 Jan 2025 18:27:57 +0530 Subject: [PATCH 47/78] test: add new unit test to check payments amount of pos_invoice returns (cherry picked from commit 484ecf24796ecf42d6fe01a23d00ab9245f9a56c) --- .../sales_invoice/test_sales_invoice.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 439fc5639e5d..b624be5cf719 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4245,6 +4245,30 @@ def test_total_billed_amount(self): doc = frappe.get_doc("Project", project.name) self.assertEqual(doc.total_billed_amount, si.grand_total) + def test_pos_returns_with_party_account_currency(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + pos_profile = make_pos_profile() + pos_profile.payments = [] + pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) + pos_profile.save() + + pos = create_sales_invoice( + customer="_Test Customer USD", + currency="USD", + conversion_rate=86.595000000, + qty=2, + do_not_save=True, + ) + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + pos.debit_to = "_Test Receivable USD - _TC" + pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35}) + pos.save().submit() + + pos_return = make_sales_return(pos.name) + self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From b004855e7cfe522cf138f803ca46840f5f018b60 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 27 Jan 2025 14:31:44 +0530 Subject: [PATCH 48/78] Revert "feat(Sales Invoice): allow linking to project without adding timesheets (backport #44295)" (#45531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "feat(Sales Invoice): allow linking to project without adding timeshee…" This reverts commit 2c2a25ab16f5b53ddf17dc23abff6e9ab14f1e4d. --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++------- erpnext/projects/doctype/timesheet/test_timesheet.py | 1 - 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 97871ba7d6fd..6e039b4b34fb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1083,16 +1083,13 @@ def set_billing_hours_and_amount(self): timesheet.billing_amount = ts_doc.total_billable_amount def update_timesheet_billing_for_project(self): - if self.timesheets: + if not self.timesheets and self.project: + self.add_timesheet_data() + else: self.calculate_billing_amount_for_timesheet() - @frappe.whitelist(methods=["PUT"]) + @frappe.whitelist() def add_timesheet_data(self): - if not self.timesheets and self.project: - self._add_timesheet_data() - self.save() - - def _add_timesheet_data(self): self.set("timesheets", []) if self.project: for data in get_projectwise_timesheet_data(self.project): diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 39140b335c93..da042f36aef1 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -62,7 +62,6 @@ def test_timesheet_billing_based_on_project(self): ) sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = project - sales_invoice._add_timesheet_data() sales_invoice.submit() ts = frappe.get_doc("Timesheet", timesheet.name) From 8f0d270746f1137cf464ea3ec53eee3aa5835cb1 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:41:52 +0530 Subject: [PATCH 49/78] feat: add company level validation for accounting dimension (cherry picked from commit 60efd3e2195a1e87cade172786ce38917fe9ab8f) --- .../accounting_dimension.json | 3 +- erpnext/controllers/accounts_controller.py | 37 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json index 5858f10bb0b4..f05d20a0a497 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json @@ -31,7 +31,8 @@ "label": "Reference Document Type", "options": "DocType", "read_only_depends_on": "eval:!doc.__islocal", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "default": "0", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8361d4d825c1..a1ee82a14991 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -8,7 +8,7 @@ import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied -from frappe.query_builder import Criterion +from frappe.query_builder import Criterion, DocType from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -266,6 +266,7 @@ def validate(self): self.set_total_in_words() self.set_default_letter_head() + self.validate_company_in_accounting_dimension() def set_default_letter_head(self): if hasattr(self, "letter_head") and not self.letter_head: @@ -403,6 +404,40 @@ def remove_serial_and_batch_bundle(self): for row in batches: frappe.delete_doc("Batch", row.name) + def validate_company_in_accounting_dimension(self): + doc_field = DocType("DocField") + accounting_dimension = DocType("Accounting Dimension") + query = ( + frappe.qb.from_(accounting_dimension) + .select(accounting_dimension.document_type) + .join(doc_field) + .on(doc_field.parent == accounting_dimension.document_type) + .where(doc_field.fieldname == "company") + ).run(as_list=True) + + dimension_list = sum(query, ["Project"]) + self.validate_company(dimension_list) + + if childs := self.get_all_children(): + for child in childs: + self.validate_company(dimension_list, child) + + def validate_company(self, dimension_list, child=None): + for dimension in dimension_list: + if not child: + dimension_value = self.get(frappe.scrub(dimension)) + else: + dimension_value = child.get(frappe.scrub(dimension)) + + if dimension_value: + company = frappe.get_cached_value(dimension, dimension_value, "company") + if company and company != self.company: + frappe.throw( + _("{0}: {1} does not belong to the Company: {2}").format( + dimension, frappe.bold(dimension_value), self.company + ) + ) + def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" From fac4e99b0ea43a65944840a0cfc6cef921a72ee8 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:45:51 +0530 Subject: [PATCH 50/78] test: add new unit test for company validation in accounting dimension (cherry picked from commit c94091d68ffc1453b9042f6f2f675d81dcd76849) --- .../tests/test_accounts_controller.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index c45923b2fedb..2c46f04af717 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer +from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item @@ -1532,32 +1533,32 @@ def test_90_dimensions_filter(self): # Invoices si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si1.department = "Management" + si1.department = "Management - _TC" si1.save().submit() si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si2.department = "Operations" + si2.department = "Operations - _TC" si2.save().submit() # Payments cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note1.department = "Management" + cr_note1.department = "Management - _TC" cr_note1.is_return = 1 cr_note1.save().submit() cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note2.department = "Legal" + cr_note2.department = "Legal - _TC" cr_note2.is_return = 1 cr_note2.save().submit() pe1 = get_payment_entry(si1.doctype, si1.name) pe1.references = [] - pe1.department = "Research & Development" + pe1.department = "Research & Development - _TC" pe1.save().submit() pe2 = get_payment_entry(si1.doctype, si1.name) pe2.references = [] - pe2.department = "Management" + pe2.department = "Management - _TC" pe2.save().submit() je1 = self.create_journal_entry( @@ -1570,7 +1571,7 @@ def test_90_dimensions_filter(self): ) je1.accounts[0].party_type = "Customer" je1.accounts[0].party = self.customer - je1.accounts[0].department = "Management" + je1.accounts[0].department = "Management - _TC" je1.save().submit() # assert dimension filter's result @@ -1579,17 +1580,17 @@ def test_90_dimensions_filter(self): self.assertEqual(len(pr.invoices), 2) self.assertEqual(len(pr.payments), 5) - pr.department = "Legal" + pr.department = "Legal - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) - pr.department = "Management" + pr.department = "Management - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 3) - pr.department = "Research & Development" + pr.department = "Research & Development - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) @@ -1600,17 +1601,17 @@ def test_91_cr_note_should_inherit_dimension(self): # Invoice si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si.department = "Management" + si.department = "Management - _TC" si.save().submit() # Payment cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note.department = "Management" + cr_note.department = "Management - _TC" cr_note.is_return = 1 cr_note.save().submit() pr = self.create_payment_reconciliation() - pr.department = "Management" + pr.department = "Management - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) @@ -1642,7 +1643,7 @@ def test_92_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency self.setup_dimensions() rate_in_account_currency = 1 - dpt = "Research & Development" + dpt = "Research & Development - _TC" si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) si.department = dpt @@ -1677,7 +1678,7 @@ def test_92_dimension_inhertiance_exc_gain_loss(self): def test_93_dimension_inheritance_on_advance(self): self.setup_dimensions() - dpt = "Research & Development" + dpt = "Research & Development - _TC" adv = self.create_payment_entry(amount=1, source_exc_rate=85) adv.department = dpt @@ -2135,3 +2136,13 @@ def test_difference_posting_date_in_pi_and_si(self): journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent) purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name) self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date) + + def test_company_validation_in_dimension(self): + si = create_sales_invoice(do_not_submit=True) + project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"}) + si.project = project.name + self.assertRaises(frappe.ValidationError, si.save) + + si_1 = create_sales_invoice(do_not_submit=True) + si_1.items[0].project = project.name + self.assertRaises(frappe.ValidationError, si_1.save) From 149827562be1957a28cf13d303fed1d0362f41b1 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:46:17 +0530 Subject: [PATCH 51/78] fix: set company related values (cherry picked from commit 454067198ee8a8f649cc9db7109032587c43a098) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b624be5cf719..1c33246ee681 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4235,6 +4235,7 @@ def test_total_billed_amount(self): si = create_sales_invoice(do_not_submit=True) project = frappe.new_doc("Project") + project.company = "_Test Company" project.project_name = "Test Total Billed Amount" project.save() From 67e45cf002bb8b4ca60aba93ff69a9ef9a9d2b11 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Tue, 21 Jan 2025 17:38:24 +0530 Subject: [PATCH 52/78] chore: update variable names for improved readability (cherry picked from commit 36bae552992f4d9009e96397db052e6e85cc2775) --- erpnext/controllers/accounts_controller.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a1ee82a14991..30c8525fe4bb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -407,7 +407,7 @@ def remove_serial_and_batch_bundle(self): def validate_company_in_accounting_dimension(self): doc_field = DocType("DocField") accounting_dimension = DocType("Accounting Dimension") - query = ( + dimension_list = ( frappe.qb.from_(accounting_dimension) .select(accounting_dimension.document_type) .join(doc_field) @@ -415,12 +415,11 @@ def validate_company_in_accounting_dimension(self): .where(doc_field.fieldname == "company") ).run(as_list=True) - dimension_list = sum(query, ["Project"]) + dimension_list = sum(dimension_list, ["Project"]) self.validate_company(dimension_list) - if childs := self.get_all_children(): - for child in childs: - self.validate_company(dimension_list, child) + for child in self.get_all_children() or []: + self.validate_company(dimension_list, child) def validate_company(self, dimension_list, child=None): for dimension in dimension_list: From bcd3351999aefecbf3b8e5b3115450eda392924c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Jan 2025 15:46:39 +0530 Subject: [PATCH 53/78] refactor(test): update test data --- erpnext/projects/doctype/project/test_records.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/test_records.json b/erpnext/projects/doctype/project/test_records.json index 567f359b50d5..1482336631ba 100644 --- a/erpnext/projects/doctype/project/test_records.json +++ b/erpnext/projects/doctype/project/test_records.json @@ -1,6 +1,7 @@ [ { "project_name": "_Test Project", - "status": "Open" + "status": "Open", + "company": "_Test Company" } ] \ No newline at end of file From e73aab0df548916551750c4371f18770b0c7c204 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Mon, 20 Jan 2025 18:01:31 +0530 Subject: [PATCH 54/78] fix: use user defined discount amount or default (cherry picked from commit e2a32b72578c2ed3dd54297cca54f293c4131a25) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 ++-- erpnext/public/js/controllers/transaction.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 385cc1a685ed..87d4d2a33e6b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -415,8 +415,8 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get("child_docname"), - "discount_percentage": 0.0, - "discount_amount": 0, + "discount_percentage": args.get("discount_percentage") or 0.0, + "discount_amount": args.get("discount_amount") or 0.0, } ) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d63461c23e00..09e52932d89f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1751,7 +1751,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "serial_no": d.serial_no, "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, - "conversion_factor": d.conversion_factor || 1.0 + "conversion_factor": d.conversion_factor || 1.0, + "discount_percentage" : d.discount_percentage, + "discount_amount" : d.discount_amount, }); // if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list From 4e347d835e86c644dfd2a0a6b955f0b4aa8e4713 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Thu, 23 Jan 2025 11:55:12 +0530 Subject: [PATCH 55/78] fix: remove applied pricing rule (cherry picked from commit 50223c6bec3e5a497034246945aa0188a415921b) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 2 -- erpnext/public/js/controllers/transaction.js | 11 +++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 87d4d2a33e6b..73cb24838118 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -415,8 +415,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get("child_docname"), - "discount_percentage": args.get("discount_percentage") or 0.0, - "discount_amount": args.get("discount_amount") or 0.0, } ) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 09e52932d89f..86c9ef46c785 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1658,7 +1658,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if (!r.exc && r.message) { - me.remove_pricing_rule(r.message, removed_pricing_rule); + me.remove_pricing_rule(r.message, removed_pricing_rule, item.name); me.calculate_taxes_and_totals(); if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); } @@ -1937,7 +1937,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } - remove_pricing_rule(item, removed_pricing_rule) { + remove_pricing_rule(item, removed_pricing_rule, row_name) { let me = this; const fields = ["discount_percentage", "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; @@ -1976,6 +1976,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.trigger_price_list_rate(); } + else if(!item.is_free_item && row_name){ + me.frm.doc.items.forEach(d => { + if (d.name != row_name) return; + + Object.assign(d, item); + }); + } } trigger_price_list_rate() { From efc7b9ac56545d236d3069a8911acd1216036b96 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jan 2025 18:14:19 +0530 Subject: [PATCH 56/78] feat: Add corrective job card operating cost as additional costs in stock entry (cherry picked from commit 2bf10f68a85fd3256fc93815e51eea311aa771c7) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- erpnext/manufacturing/doctype/bom/bom.py | 49 +++++++++++++++++++ .../doctype/job_card/job_card.py | 8 +++ 2 files changed, 57 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6b2dd77c471b..f0f9b0c354cc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1371,6 +1371,55 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): }, ) + def get_max_op_qty(): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Job Card") + query = ( + frappe.qb.from_(table) + .select(Sum(table.total_completed_qty).as_("qty")) + .where( + (table.docstatus == 1) + & (table.work_order == work_order.name) + & (table.is_corrective_job_card == 0) + ) + .groupby(table.operation) + ) + return min([d.qty for d in query.run(as_dict=True)], default=0) + + def get_utilised_cc(): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Stock Entry") + subquery = ( + frappe.qb.from_(table) + .select(table.name) + .where( + (table.docstatus == 1) + & (table.work_order == work_order.name) + & (table.purpose == "Manufacture") + ) + ) + table = frappe.qb.DocType("Landed Cost Taxes and Charges") + query = ( + frappe.qb.from_(table) + .select(Sum(table.amount).as_("amount")) + .where(table.parent.isin(subquery) & (table.description == "Corrective Operation Cost")) + ) + return query.run(as_dict=True)[0].amount or 0 + + if work_order and work_order.corrective_operation_cost: + max_qty = get_max_op_qty() - work_order.produced_qty + remaining_cc = work_order.corrective_operation_cost - get_utilised_cc() + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": "Corrective Operation Cost", + "amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty), + }, + ) + @frappe.whitelist() def get_bom_diff(bom1, bom2): diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a1b53fb7c4a4..893c698ff5f1 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,7 +650,15 @@ def get_required_items(self): ) ) +<<<<<<< HEAD if self.get("operation") == d.operation: +======= + if ( + self.get("operation") == d.operation + or self.operation_row_id == d.operation_row_id + or self.is_corrective_job_card + ): +>>>>>>> 2bf10f68a8 (feat: Add corrective job card operating cost as additional costs in stock entry) self.append( "items", { From 5c9ac274783d4f169897c3838cfad56430ddfbaf Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 10:08:47 +0530 Subject: [PATCH 57/78] test: Added test for new feature (cherry picked from commit 4fb48b7f226ecd6c5a9a23511584a1953831c1fd) --- .../doctype/job_card/test_job_card.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index d6d3775111de..58ed1d5a4bb4 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -424,6 +424,46 @@ def test_corrective_costing(self): cost_after_cancel = self.work_order.total_operating_cost self.assertEqual(cost_after_cancel, original_cost) + @IntegrationTestCase.change_settings( + "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} + ) + def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + corrective_action = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 100 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + self.work_order.reload() + + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + + stock_entry = make_stock_entry_for_wo(self.work_order.name, "Manufacture") + self.assertEqual(stock_entry.additional_costs[1].description, "Corrective Operation Cost") + self.assertEqual(stock_entry.additional_costs[1].amount, 50) + self.assertEqual(stock_entry["items"][-1].additional_cost, 6050) + def test_job_card_statuses(self): def assertStatus(status): jc.set_status() From d6e0c6c96954bbb40f413cf75b593c7148a054e2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 20:52:44 +0530 Subject: [PATCH 58/78] refactor: added condition which checks for corrective operation setting (cherry picked from commit 063a205e5a29f48457a0a8ad248b0788dbeda5a6) --- erpnext/manufacturing/doctype/bom/bom.py | 10 +++- .../doctype/job_card/test_job_card.py | 60 ++++++++++++++++--- .../stock/doctype/stock_entry/stock_entry.py | 11 ---- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index f0f9b0c354cc..53ec6fc1e8ba 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1408,7 +1408,15 @@ def get_utilised_cc(): ) return query.run(as_dict=True)[0].amount or 0 - if work_order and work_order.corrective_operation_cost: + if ( + work_order + and work_order.corrective_operation_cost + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ) + ): max_qty = get_max_op_qty() - work_order.produced_qty remaining_cc = work_order.corrective_operation_cost - get_utilised_cc() stock_entry.append( diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 58ed1d5a4bb4..03c605ce156c 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -428,10 +428,18 @@ def test_corrective_costing(self): "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} ) def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): - job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + wo = make_wo_order_test_record( + item="_Test FG Item 2", + qty=10, + transfer_material_against=self.transfer_material_against, + source_warehouse=self.source_warehouse, + ) + self.generate_required_stock(wo) + job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) + job_card.update({"for_quantity": 4}) job_card.append( "time_logs", - {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 4}, ) job_card.submit() @@ -449,20 +457,56 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): { "from_time": add_to_date(now(), hours=2), "to_time": add_to_date(now(), hours=2, minutes=30), - "completed_qty": 2, + "completed_qty": 4, }, ) corrective_job_card.submit() - self.work_order.reload() + wo.reload() from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_stock_entry_for_wo, ) - stock_entry = make_stock_entry_for_wo(self.work_order.name, "Manufacture") - self.assertEqual(stock_entry.additional_costs[1].description, "Corrective Operation Cost") - self.assertEqual(stock_entry.additional_costs[1].amount, 50) - self.assertEqual(stock_entry["items"][-1].additional_cost, 6050) + stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=3) + self.assertEqual(stock_entry.additional_costs[1].amount, 37.5) + frappe.get_doc(stock_entry).submit() + + from erpnext.manufacturing.doctype.work_order.work_order import make_job_card + + make_job_card( + wo.name, + [{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}], + ) + job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) + job_card.update({"for_quantity": 3}) + job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=3), + "to_time": add_to_date(now(), hours=4), + "completed_qty": 3, + }, + ) + job_card.submit() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 80 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=4), + "to_time": add_to_date(now(), hours=4, minutes=30), + "completed_qty": 3, + }, + ) + corrective_job_card.submit() + wo.reload() + + stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=4) + self.assertEqual(stock_entry.additional_costs[1].amount, 52.5) def test_job_card_statuses(self): def assertStatus(status): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2da35c0ef25..eec30f69f36a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2868,17 +2868,6 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - if ( - work_order - and work_order.produced_qty - and cint( - frappe.db.get_single_value( - "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" - ) - ) - ): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) - return operating_cost_per_unit From c102e51eb18748b60f5793b23bd6dfd35b66835b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 20 Jan 2025 12:41:28 +0530 Subject: [PATCH 59/78] fix: logical error in where condition of qb query (cherry picked from commit 47f8a8600374a6a130c45c12e65f1cf11fa450e0) # Conflicts: # erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- .../landed_cost_taxes_and_charges.json | 14 +++++++++++++- .../landed_cost_taxes_and_charges.py | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 53ec6fc1e8ba..5d13471f5413 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1404,7 +1404,7 @@ def get_utilised_cc(): query = ( frappe.qb.from_(table) .select(Sum(table.amount).as_("amount")) - .where(table.parent.isin(subquery) & (table.description == "Corrective Operation Cost")) + .where(table.parent.isin(subquery) & (table.has_corrective_cost == 1)) ) return query.run(as_dict=True)[0].amount or 0 @@ -1424,6 +1424,7 @@ def get_utilised_cc(): { "expense_account": expense_account, "description": "Corrective Operation Cost", + "has_corrective_cost": 1, "amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty), }, ) diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 9c59c13ac071..2d5823d3d519 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -11,7 +11,8 @@ "description", "col_break3", "amount", - "base_amount" + "base_amount", + "has_corrective_cost" ], "fields": [ { @@ -62,12 +63,23 @@ "label": "Amount (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "has_corrective_cost", + "fieldtype": "Check", + "label": "Has Corrective Cost", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-05-17 13:57:10.807980", +======= + "modified": "2025-01-20 12:22:03.455762", +>>>>>>> 47f8a86003 (fix: logical error in where condition of qb query) "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py index 8509cb71d853..a3f7f037d607 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py @@ -20,6 +20,7 @@ class LandedCostTaxesandCharges(Document): description: DF.SmallText exchange_rate: DF.Float expense_account: DF.Link | None + has_corrective_cost: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data From fb1ea9524b22547cd672df9ca54f92e234a3a732 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:29:51 +0100 Subject: [PATCH 60/78] chore: bump actions/cache to v4 (#45541) --- .github/workflows/patch.yml | 6 +++--- .github/workflows/server-tests-mariadb.yml | 6 +++--- .github/workflows/server-tests-postgres.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 21dd3d487946..ffaa6b5e7df6 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -57,7 +57,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -66,7 +66,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -81,7 +81,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 720f0e0ed350..8f2dfe0c0e87 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -76,7 +76,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -85,7 +85,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -100,7 +100,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index a68870665701..2470438aa7c2 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -66,7 +66,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -75,7 +75,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -90,7 +90,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} From 57f79a22401ec1ffadb8ae4a7dddea5236e213db Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:01:13 +0530 Subject: [PATCH 61/78] fix: merge conflict --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ---- .../landed_cost_taxes_and_charges.json | 4 ---- 2 files changed, 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 893c698ff5f1..50bec3286207 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,15 +650,11 @@ def get_required_items(self): ) ) -<<<<<<< HEAD - if self.get("operation") == d.operation: -======= if ( self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id or self.is_corrective_job_card ): ->>>>>>> 2bf10f68a8 (feat: Add corrective job card operating cost as additional costs in stock entry) self.append( "items", { diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 2d5823d3d519..898848ebf422 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -75,11 +75,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-05-17 13:57:10.807980", -======= "modified": "2025-01-20 12:22:03.455762", ->>>>>>> 47f8a86003 (fix: logical error in where condition of qb query) "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", From d74c498efecbe3ff80d20782192f046be68c36d5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:17:13 +0530 Subject: [PATCH 62/78] fix: import --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 03c605ce156c..31864c9262c9 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -6,6 +6,7 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests import IntegrationTestCase from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string from frappe.utils.data import add_to_date, now, today From b59d253d93fa521994c22c81600e7e7e9c8ee6ff Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:55:33 +0530 Subject: [PATCH 63/78] fix: import 2 --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 31864c9262c9..5e381bd06ffb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -6,7 +6,6 @@ import frappe from frappe.test_runner import make_test_records -from frappe.tests import IntegrationTestCase from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string from frappe.utils.data import add_to_date, now, today @@ -425,7 +424,7 @@ def test_corrective_costing(self): cost_after_cancel = self.work_order.total_operating_cost self.assertEqual(cost_after_cancel, original_cost) - @IntegrationTestCase.change_settings( + @change_settings( "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} ) def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): From 1be19819fbc79482004e53c15567ef3ae38b4966 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jan 2025 09:26:14 +0530 Subject: [PATCH 64/78] fix: removed field not present in v15 --- erpnext/manufacturing/doctype/job_card/job_card.py | 6 +----- erpnext/manufacturing/doctype/job_card/test_job_card.py | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 50bec3286207..0f0694e33b0d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,11 +650,7 @@ def get_required_items(self): ) ) - if ( - self.get("operation") == d.operation - or self.operation_row_id == d.operation_row_id - or self.is_corrective_job_card - ): + if self.get("operation") == d.operation or self.is_corrective_job_card: self.append( "items", { diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 5e381bd06ffb..0119e74cb4c5 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -451,6 +451,7 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): job_card.name, operation=corrective_action.name, for_operation=job_card.operation ) corrective_job_card.hour_rate = 100 + corrective_job_card.update({"hour_rate": 100}) corrective_job_card.insert() corrective_job_card.append( "time_logs", @@ -460,6 +461,7 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): "completed_qty": 4, }, ) + print(corrective_job_card.as_dict()) corrective_job_card.submit() wo.reload() From f60a3bcedfe48d84a9773f504a99cd50c83a0b76 Mon Sep 17 00:00:00 2001 From: Sanket Shah <113279972+Sanket322@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:27:28 +0530 Subject: [PATCH 65/78] fix: update fields on change of item code In `Update Items` of `Sales Order` (#45125) * fix: update fields on change of item code * fix: minor update * fix: set the new values always * Revert "fix: set the new values always" This reverts commit 44daa0a641e489515eb73fc35fcea308d6cb49e8. --------- Co-authored-by: Sanket322 Co-authored-by: ruthra kumar (cherry picked from commit 9933d3c8ff047a28dbfb8cf1f76d8f283a204376) --- erpnext/public/js/utils.js | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 7c02fefc0f96..a000cdee7cce 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -654,6 +654,62 @@ erpnext.utils.update_child_items = function (opts) { filters: filters, }; }, + onchange: function () { + const me = this; + + frm.call({ + method: "erpnext.stock.get_item_details.get_item_details", + args: { + doc: frm.doc, + ctx: { + item_code: this.value, + set_warehouse: frm.doc.set_warehouse, + customer: frm.doc.customer || frm.doc.party_name, + quotation_to: frm.doc.quotation_to, + supplier: frm.doc.supplier, + currency: frm.doc.currency, + is_internal_supplier: frm.doc.is_internal_supplier, + is_internal_customer: frm.doc.is_internal_customer, + conversion_rate: frm.doc.conversion_rate, + price_list: frm.doc.selling_price_list || frm.doc.buying_price_list, + price_list_currency: frm.doc.price_list_currency, + plc_conversion_rate: frm.doc.plc_conversion_rate, + company: frm.doc.company, + order_type: frm.doc.order_type, + is_pos: cint(frm.doc.is_pos), + is_return: cint(frm.doc.is_return), + is_subcontracted: frm.doc.is_subcontracted, + ignore_pricing_rule: frm.doc.ignore_pricing_rule, + doctype: frm.doc.doctype, + name: frm.doc.name, + qty: me.doc.qty || 1, + uom: me.doc.uom, + pos_profile: cint(frm.doc.is_pos) ? frm.doc.pos_profile : "", + tax_category: frm.doc.tax_category, + child_doctype: frm.doc.doctype + " Item", + is_old_subcontracting_flow: frm.doc.is_old_subcontracting_flow, + }, + }, + callback: function (r) { + if (r.message) { + const { qty, price_list_rate: rate, uom, conversion_factor } = r.message; + + const row = dialog.fields_dict.trans_items.df.data.find( + (doc) => doc.idx == me.doc.idx + ); + if (row) { + Object.assign(row, { + conversion_factor: me.doc.conversion_factor || conversion_factor, + uom: me.doc.uom || uom, + qty: me.doc.qty || qty, + rate: me.doc.rate || rate, + }); + dialog.fields_dict.trans_items.grid.refresh(); + } + } + }, + }); + }, }, { fieldtype: "Link", From de43c123e206085545b4c075882b6839c225962e Mon Sep 17 00:00:00 2001 From: eagleautomate <167297807+eagleautomate@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:29:36 +0530 Subject: [PATCH 66/78] feat: Add chart of accounts for Switzerland 240812 Schulkontenrahmen VEB - DE (cherry picked from commit 2c644ec2ef888c20b61321a7e927ec9519e5bb15) --- .../ch_240812 Schulkontenrahmen VEB - DE.json | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json new file mode 100644 index 000000000000..dff27a213c11 --- /dev/null +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json @@ -0,0 +1,532 @@ +{ + "country_code": "ch", + "name": "240812 Schulkontenrahmen VEB - DE", + "tree": { + "Aktiven": { + "account_number": "1", + "is_group": 1, + "root_type": "Asset", + "Umlaufvermögen": { + "account_number": "10", + "is_group": 1, + "Flüssige Mittel": { + "account_number": "100", + "is_group": 1, + "Kasse": { + "account_number": "1000", + "account_type": "Cash" + }, + "Bankguthaben": { + "account_number": "1020", + "account_type": "Bank" + } + }, + "Kurzfristig gehaltene Aktiven mit Börsenkurs": { + "account_number": "106", + "is_group": 1, + "Wertschriften": { + "account_number": "1060" + }, + "Wertberichtigungen Wertschriften": { + "account_number": "1069" + } + }, + "Forderungen aus Lieferungen und Leistungen": { + "account_number": "110", + "is_group": 1, + "Forderungen aus Lieferungen und Leistungen (Debitoren)": { + "account_number": "1100" + }, + "Delkredere": { + "account_number": "1109" + } + }, + "Übrige kurzfristige Forderungen": { + "account_number": "114", + "is_group": 1, + "Vorschüsse und Darlehen": { + "account_number": "1140" + }, + "Wertberichtigungen Vorschüsse und Darlehen": { + "account_number": "1149" + }, + "Vorsteuer MWST Material, Waren, Dienstleistungen, Energie": { + "account_number": "1170" + }, + "Vorsteuer MWST Investitionen, übriger Betriebsaufwand": { + "account_number": "1171" + }, + "Verrechnungssteuer": { + "account_number": "1176" + }, + "Forderungen gegenüber Sozialversicherungen und Vorsorgeeinrichtungen": { + "account_number": "1180" + }, + "Quellensteuer": { + "account_number": "1189" + }, + "Sonstige kurzfristige Forderungen": { + "account_number": "1190" + }, + "Wertberichtigungen sonstige kurzfristige Forderungen": { + "account_number": "1199" + } + }, + "Vorräte und nicht fakturierte Dienstleistungen": { + "account_number": "120", + "is_group": 1, + "Handelswaren": { + "account_number": "1200" + }, + "Rohstoffe": { + "account_number": "1210" + }, + "Werkstoffe": { + "account_number": "1220" + }, + "Hilfs- und Verbrauchsmaterial": { + "account_number": "1230" + }, + "Handelswaren in Konsignation": { + "account_number": "1250" + }, + "Fertige Erzeugnisse": { + "account_number": "1260" + }, + "Unfertige Erzeugnisse": { + "account_number": "1270" + }, + "Nicht fakturierte Dienstleistungen": { + "account_number": "1280" + } + }, + "Aktive Rechnungsabgrenzungen": { + "account_number": "130", + "is_group": 1, + "Bezahlter Aufwand des Folgejahres": { + "account_number": "1300" + }, + "Noch nicht erhaltener Ertrag": { + "account_number": "1301" + } + } + }, + "Anlagevermögen": { + "account_number": "14", + "is_group": 1, + "Finanzanlagen": { + "account_number": "140", + "is_group": 1, + "Wertschriften": { + "account_number": "1400" + }, + "Wertberichtigungen Wertschriften": { + "account_number": "1409" + }, + "Darlehen": { + "account_number": "1440" + }, + "Hypotheken": { + "account_number": "1441" + }, + "Wertberichtigungen langfristige Forderungen": { + "account_number": "1449" + } + }, + "Beteiligungen": { + "account_number": "148", + "is_group": 1, + "Beteiligungen": { + "account_number": "1480" + }, + "Wertberichtigungen Beteiligungen": { + "account_number": "1489" + } + }, + "Mobile Sachanlagen": { + "account_number": "150", + "is_group": 1, + "Maschinen und Apparate": { + "account_number": "1500" + }, + "Wertberichtigungen Maschinen und Apparate": { + "account_number": "1509" + }, + "Mobiliar und Einrichtungen": { + "account_number": "1510" + }, + "Wertberichtigungen Mobiliar und Einrichtungen": { + "account_number": "1519" + }, + "Büromaschinen, Informatik, Kommunikationstechnologie": { + "account_number": "1520" + }, + "Wertberichtigungen Büromaschinen, Informatik, Kommunikationstechnologie": { + "account_number": "1529" + }, + "Fahrzeuge": { + "account_number": "1530" + }, + "Wertberichtigungen Fahrzeuge": { + "account_number": "1539" + }, + "Werkzeuge und Geräte": { + "account_number": "1540" + }, + "Wertberichtigungen Werkzeuge und Geräte": { + "account_number": "1549" + } + }, + "Immobile Sachanlagen": { + "account_number": "160", + "is_group": 1, + "Geschäftsliegenschaften": { + "account_number": "1600" + }, + "Wertberichtigungen Geschäftsliegenschaften": { + "account_number": "1609" + } + }, + "Immaterielle Werte": { + "account_number": "170", + "is_group": 1, + "Patente, Know-how, Lizenzen, Rechte, Entwicklungen": { + "account_number": "1700" + }, + "Wertberichtigungen Patente, Know-how, Lizenzen, Rechte, Entwicklungen": { + "account_number": "1709" + }, + "Goodwill": { + "account_number": "1770" + }, + "Wertberichtigungen Goodwill": { + "account_number": "1779" + } + }, + "Nicht einbezahltes Grund-, Gesellschafter- oder Stiftungskapital": { + "account_number": "180", + "is_group": 1, + "Nicht einbezahltes Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": { + "account_number": "1850" + } + } + } + }, + "Passiven": { + "account_number": "2", + "is_group": 1, + "root_type": "Liability", + "Kurzfristiges Fremdkapital": { + "account_number": "20", + "is_group": 1, + "Verbindlichkeiten aus Lieferungen und Leistungen": { + "account_number": "200", + "is_group": 1, + "Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)": { + "account_number": "2000" + }, + "Erhaltene Anzahlungen": { + "account_number": "2030" + } + }, + "Kurzfristige verzinsliche Verbindlichkeiten": { + "account_number": "210", + "is_group": 1, + "Bankverbindlichkeiten": { + "account_number": "2100" + }, + "Verbindlichkeiten aus Finanzierungsleasing": { + "account_number": "2120" + }, + "Übrige verzinsliche Verbindlichkeiten": { + "account_number": "2140" + } + }, + "Übrige kurzfristige Verbindlichkeiten": { + "account_number": "220", + "is_group": 1, + "Geschuldete MWST (Umsatzsteuer)": { + "account_number": "2200" + }, + "Abrechnungskonto MWST": { + "account_number": "2201" + }, + "Verrechnungssteuer": { + "account_number": "2206" + }, + "Direkte Steuern": { + "account_number": "2208" + }, + "Sonstige kurzfristige Verbindlichkeiten": { + "account_number": "2210" + }, + "Beschlossene Ausschüttungen": { + "account_number": "2261" + }, + "Sozialversicherungen und Vorsorgeeinrichtungen": { + "account_number": "2270" + }, + "Quellensteuer": { + "account_number": "2279" + } + }, + "Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen": { + "account_number": "230", + "is_group": 1, + "Noch nicht bezahlter Aufwand": { + "account_number": "2300" + }, + "Erhaltener Ertrag des Folgejahres": { + "account_number": "2301" + }, + "Kurzfristige Rückstellungen": { + "account_number": "2330" + } + } + }, + "Langfristiges Fremdkapital": { + "account_number": "24", + "is_group": 1, + "Langfristige verzinsliche Verbindlichkeiten": { + "account_number": "240", + "is_group": 1, + "Bankverbindlichkeiten": { + "account_number": "2400" + }, + "Verbindlichkeiten aus Finanzierungsleasing": { + "account_number": "2420" + }, + "Obligationenanleihen": { + "account_number": "2430" + }, + "Darlehen": { + "account_number": "2450" + }, + "Hypotheken": { + "account_number": "2451" + } + }, + "Übrige langfristige Verbindlichkeiten": { + "account_number": "250", + "is_group": 1, + "Übrige langfristige Verbindlichkeiten (unverzinslich)": { + "account_number": "2500" + } + }, + "Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen": { + "account_number": "260", + "is_group": 1, + "Rückstellungen": { + "account_number": "2600" + } + } + }, + "Eigenkapital (juristische Personen)": { + "account_number": "28", + "is_group": 1, + "Grund-, Gesellschafter- oder Stiftungskapital": { + "account_number": "280", + "is_group": 1, + "Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": { + "account_number": "2800" + } + }, + "Reserven und Jahresgewinn oder Jahresverlust": { + "account_number": "290", + "is_group": 1, + "Gesetzliche Kapitalreserve": { + "account_number": "2900" + }, + "Reserve für eigene Kapitalanteile": { + "account_number": "2930" + }, + "Aufwertungsreserve": { + "account_number": "2940" + }, + "Gesetzliche Gewinnreserve": { + "account_number": "2950" + }, + "Freiwillige Gewinnreserven": { + "account_number": "2960" + }, + "Gewinnvortrag oder Verlustvortrag": { + "account_number": "2970" + }, + "Jahresgewinn oder Jahresverlust": { + "account_number": "2979" + }, + "Eigene Aktien, Stammanteile oder Anteilscheine (Minusposten)": { + "account_number": "2980" + } + } + } + }, + "Betrieblicher Ertrag aus Lieferungen und Leistungen": { + "account_number": "3", + "is_group": 1, + "root_type": "Income", + "Produktionserlöse": { + "account_number": "3000" + }, + "Handelserlöse": { + "account_number": "3200" + }, + "Dienstleistungserlöse": { + "account_number": "3400" + }, + "Übrige Erlöse aus Lieferungen und Leistungen": { + "account_number": "3600" + }, + "Eigenleistungen": { + "account_number": "3700" + }, + "Eigenverbrauch": { + "account_number": "3710" + }, + "Erlösminderungen": { + "account_number": "3800" + }, + "Verluste Forderungen (Debitoren), Veränderung Delkredere": { + "account_number": "3805" + }, + "Bestandesänderungen unfertige Erzeugnisse": { + "account_number": "3900" + }, + "Bestandesänderungen fertige Erzeugnisse": { + "account_number": "3901" + }, + "Bestandesänderungen nicht fakturierte Dienstleistungen": { + "account_number": "3940" + } + }, + "Aufwand für Material, Handelswaren, Dienstleistungen und Energie": { + "account_number": "4", + "is_group": 1, + "root_type": "Expense", + "Materialaufwand Produktion": { + "account_number": "4000" + }, + "Handelswarenaufwand": { + "account_number": "4200" + }, + "Aufwand für bezogene Dienstleistungen": { + "account_number": "4400" + }, + "Energieaufwand zur Leistungserstellung": { + "account_number": "4500" + }, + "Aufwandminderungen": { + "account_number": "4900" + } + }, + "Personalaufwand": { + "account_number": "5", + "is_group": 1, + "root_type": "Expense", + "Lohnaufwand": { + "account_number": "5000" + }, + "Sozialversicherungsaufwand": { + "account_number": "5700" + }, + "Übriger Personalaufwand": { + "account_number": "5800" + }, + "Leistungen Dritter": { + "account_number": "5900" + } + }, + "Übriger betrieblicher Aufwand, Abschreibungen und Wertberichtigungen sowie Finanzergebnis": { + "account_number": "6", + "is_group": 1, + "root_type": "Expense", + "Raumaufwand": { + "account_number": "6000" + }, + "Unterhalt, Reparaturen, Ersatz mobile Sachanlagen": { + "account_number": "6100" + }, + "Leasingaufwand mobile Sachanlagen": { + "account_number": "6105" + }, + "Fahrzeug- und Transportaufwand": { + "account_number": "6200" + }, + "Fahrzeugleasing und -mieten": { + "account_number": "6260" + }, + "Sachversicherungen, Abgaben, Gebühren, Bewilligungen": { + "account_number": "6300" + }, + "Energie- und Entsorgungsaufwand": { + "account_number": "6400" + }, + "Verwaltungsaufwand": { + "account_number": "6500" + }, + "Informatikaufwand inkl. Leasing": { + "account_number": "6570" + }, + "Werbeaufwand": { + "account_number": "6600" + }, + "Sonstiger betrieblicher Aufwand": { + "account_number": "6700" + }, + "Abschreibungen und Wertberichtigungen auf Positionen des Anlagevermögens": { + "account_number": "6800" + }, + "Finanzaufwand": { + "account_number": "6900" + }, + "Finanzertrag": { + "account_number": "6950" + } + }, + "Betrieblicher Nebenerfolg": { + "account_number": "7", + "is_group": 1, + "root_type": "Income", + "Ertrag Nebenbetrieb": { + "account_number": "7000" + }, + "Aufwand Nebenbetrieb": { + "account_number": "7010" + }, + "Ertrag betriebliche Liegenschaft": { + "account_number": "7500" + }, + "Aufwand betriebliche Liegenschaft": { + "account_number": "7510" + } + }, + "Betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag": { + "account_number": "8", + "is_group": 1, + "root_type": "Expense", + "Betriebsfremder Aufwand": { + "account_number": "8000" + }, + "Betriebsfremder Ertrag": { + "account_number": "8100" + }, + "Ausserordentlicher, einmaliger oder periodenfremder Aufwand": { + "account_number": "8500" + }, + "Ausserordentlicher, einmaliger oder periodenfremder Ertrag": { + "account_number": "8510" + }, + "Direkte Steuern": { + "account_number": "8900" + } + }, + "Abschluss": { + "account_number": "9", + "is_group": 1, + "root_type": "Equity", + "Jahresgewinn oder Jahresverlust": { + "account_number": "9200" + } + } + } +} \ No newline at end of file From eef907a275c48764ce22da70cad4b5e94c48b556 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Jan 2025 11:41:14 +0530 Subject: [PATCH 67/78] chore: rename json to standard name format --- ...hmen VEB - DE.json => ch_240812_schulkontenrahmen_veb_de.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename erpnext/accounts/doctype/account/chart_of_accounts/verified/{ch_240812 Schulkontenrahmen VEB - DE.json => ch_240812_schulkontenrahmen_veb_de.json} (100%) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812_schulkontenrahmen_veb_de.json similarity index 100% rename from erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812 Schulkontenrahmen VEB - DE.json rename to erpnext/accounts/doctype/account/chart_of_accounts/verified/ch_240812_schulkontenrahmen_veb_de.json From b37602c716b6bad3ae3485f5a578e7c0a0e78c38 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Sun, 26 Jan 2025 19:52:06 +0530 Subject: [PATCH 68/78] fix(payment entry): get amount in transaction currency (cherry picked from commit af97f4242909133cc7c8833506dbc4fcec1e21d2) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a2a4a5185cf0..b0090f27004a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1576,6 +1576,14 @@ def get_party_account_for_taxes(self): elif self.payment_type in ("Pay", "Internal Transfer"): return self.paid_from + def get_value_in_transaction_currency(self, account_currency, gl_dict, field): + company_currency = erpnext.get_company_currency(self.company) + conversion_rate = self.target_exchange_rate + if self.paid_from_account_currency != company_currency: + conversion_rate = self.source_exchange_rate + + return flt(gl_dict.get(field, 0) / (conversion_rate or 1)) + def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): From 6c4655dd72dc304d07b2b23a26e7f61eb1ff00b9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jan 2025 16:56:05 +0530 Subject: [PATCH 69/78] fix: existing logical error --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- erpnext/manufacturing/doctype/job_card/test_job_card.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 0f0694e33b0d..90f915d9c240 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -791,7 +791,7 @@ def update_corrective_in_work_order(self, wo): fields=["total_time_in_mins", "hour_rate"], filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order}, ): - wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + wo.corrective_operation_cost += (flt(row.total_time_in_mins) / 60) * flt(row.hour_rate) wo.calculate_operating_cost() wo.flags.ignore_validate_update_after_submit = True diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 0119e74cb4c5..7f456b9881e6 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -451,7 +451,6 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): job_card.name, operation=corrective_action.name, for_operation=job_card.operation ) corrective_job_card.hour_rate = 100 - corrective_job_card.update({"hour_rate": 100}) corrective_job_card.insert() corrective_job_card.append( "time_logs", @@ -461,7 +460,6 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): "completed_qty": 4, }, ) - print(corrective_job_card.as_dict()) corrective_job_card.submit() wo.reload() @@ -479,8 +477,10 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): wo.name, [{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}], ) + workstation = job_card.workstation job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) job_card.update({"for_quantity": 3}) + job_card.workstation = workstation job_card.append( "time_logs", { From 8b0efab13ede54d8b5ffd9451108c77f09aff269 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:45:34 +0530 Subject: [PATCH 70/78] fix: add multiple item issue in stock entry (backport #45544) (#45580) fix: add multiple item issue in stock entry (#45544) (cherry picked from commit 5a023dc8d47dc12dcf2e3f031a409f592f54a867) Co-authored-by: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com> --- erpnext/stock/doctype/stock_entry/stock_entry.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8441b6edd640..635fd1a1fcfe 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -906,7 +906,12 @@ frappe.ui.form.on("Stock Entry Detail", { var d = locals[cdt][cdn]; $.each(r.message, function (k, v) { if (v) { - frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered + // set_value trigger barcode function and barcode set qty to 1 in stock_controller.js, to avoid this set value manually instead of set value. + if (k != "barcode") { + frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered + } else { + d.barcode = v; + } } }); refresh_field("items"); From a2ffdc78054d70c2daa7f8d00a712b8a328002c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:46:44 +0530 Subject: [PATCH 71/78] fix: return qty error due to precision (backport #45536) (#45581) fix: return qty error due to precision (cherry picked from commit 3078578692f01ff90b2f5a2a4d2955b0e528d19f) Co-authored-by: Dany Robert --- erpnext/controllers/sales_and_purchase_return.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 2079338b09ad..66ec88517272 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -174,7 +174,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items): ) for column in fields: - returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0 + returned_qty = ( + flt(already_returned_data.get(column, 0), stock_qty_precision) + if len(already_returned_data) > 0 + else 0 + ) if column == "stock_qty" and not args.get("return_qty_from_rejected_warehouse"): reference_qty = ref.get(column) @@ -186,7 +190,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items): reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0) current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0) - max_returnable_qty = flt(reference_qty, stock_qty_precision) - returned_qty + max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision) label = column.replace("_", " ").title() if reference_qty: From aef6b62f7d68520e7878497718c9ffc7502118e4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 29 Jan 2025 14:38:50 +0530 Subject: [PATCH 72/78] refactor: auto add taxes from template (cherry picked from commit d1086722bf04127fbfb4683c518a864773391e59) --- erpnext/controllers/accounts_controller.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 30c8525fe4bb..2aadd6b584ed 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -252,6 +252,8 @@ def validate(self): self.validate_deferred_income_expense_account() self.set_inter_company_account() + self.set_taxes_and_charges() + if self.doctype == "Purchase Invoice": self.calculate_paid_amount() # apply tax withholding only if checked and applicable @@ -969,6 +971,12 @@ def is_pos_profile_changed(self): ): return True + def set_taxes_and_charges(self): + if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): + if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"): + if tax_master_doctype := self.meta.get_field("taxes_and_charges").options: + self.append_taxes_from_master(tax_master_doctype) + def append_taxes_from_master(self, tax_master_doctype=None): if self.get("taxes_and_charges"): if not tax_master_doctype: From 28bb9c39e823560426cb8a727da03409ea9e7146 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sat, 25 Jan 2025 13:04:12 +0530 Subject: [PATCH 73/78] fix: show payment entries in Tax Withheld Vouchers (cherry picked from commit 55733d4f18832443f2fa38e41705b3afc3ef58b5) --- .../purchase_invoice/purchase_invoice.py | 8 +- .../tax_withholding_category.py | 139 ++++++++++-------- .../test_tax_withholding_category.py | 11 ++ 3 files changed, 90 insertions(+), 68 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2b4242aa2338..3d3481c053db 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1797,13 +1797,13 @@ def set_tax_withholding(self): self.remove(d) ## Add pending vouchers on which tax was withheld - for voucher_no, voucher_details in voucher_wise_amount.items(): + for row in voucher_wise_amount: self.append( "tax_withheld_vouchers", { - "voucher_name": voucher_no, - "voucher_type": voucher_details.get("voucher_type"), - "taxable_amount": voucher_details.get("amount"), + "voucher_name": row.voucher_name, + "voucher_type": row.voucher_type, + "taxable_amount": row.taxable_amount, }, ) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 53a2e279a4d9..9987ff5e031a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -87,6 +87,7 @@ def get_party_details(inv): def get_party_tax_withholding_details(inv, tax_withholding_category=None): if inv.doctype == "Payment Entry": inv.tax_withholding_net_total = inv.net_total + inv.base_tax_withholding_net_total = inv.net_total pan_no = "" parties = [] @@ -326,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} else: - tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers) + tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount) elif party_type == "Customer": if tax_deducted: @@ -356,13 +357,16 @@ def is_tax_deducted_on_the_basis_of_inv(vouchers): def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): - doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" - field = ( - "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total" - ) - voucher_wise_amount = {} + voucher_wise_amount = [] vouchers = [] + doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + field = [ + "base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total", + "name", + "grand_total", + ] + filters = { "company": company, frappe.scrub(party_type): ["in", parties], @@ -376,15 +380,24 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field]) + invoices_details = frappe.get_all(doctype, filters=filters, fields=field) for d in invoices_details: vouchers.append(d.name) - voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + voucher_wise_amount.append( + frappe._dict( + { + "voucher_name": d.name, + "voucher_type": doctype, + "taxable_amount": d.base_net_total, + "grand_total": d.grand_total, + } + ) + ) journal_entries_details = frappe.db.sql( """ - SELECT j.name, ja.credit - ja.debit AS amount + SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE j.name = ja.parent @@ -403,13 +416,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): tax_details.get("tax_withholding_category"), company, ), - as_dict=1, ) - if journal_entries_details: - for d in journal_entries_details: - vouchers.append(d.name) - voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) + for d in journal_entries_details: + vouchers.append(d.name) + voucher_wise_amount.append( + frappe._dict( + { + "voucher_name": d.name, + "voucher_type": "Journal Entry", + "taxable_amount": d.amount, + "reference_type": d.reference_type, + } + ) + ) return vouchers, voucher_wise_amount @@ -508,12 +528,24 @@ def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details): return advance_tax_from_across_fiscal_year -def get_tds_amount(ldc, parties, inv, tax_details, vouchers): +def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount): tds_amount = 0 - invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} + + pi_grand_total = 0 + pi_base_taxable_net_total = 0 + jv_credit_emt = 0 + pe_credit_amt = 0 + + for row in voucher_wise_amount: + if row.voucher_type == "Purchase Invoice": + pi_grand_total += row.get("grand_total", 0) + pi_base_taxable_net_total += row.get("taxable_amount", 0) + + if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice": + jv_credit_emt += row.get("taxable_amount", 0) ## for TDS to be deducted on advances - payment_entry_filters = { + pe_filters = { "party_type": "Supplier", "party": ("in", parties), "docstatus": 1, @@ -524,70 +556,49 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers): "company": inv.company, } - field = "sum(tax_withholding_net_total)" + consider_party_ledger_amount = cint(tax_details.consider_party_ledger_amount) - if cint(tax_details.consider_party_ledger_amount): - invoice_filters.pop("apply_tds", None) - field = "sum(grand_total)" - - payment_entry_filters.pop("apply_tax_withholding_amount", None) - payment_entry_filters.pop("tax_withholding_category", None) - - supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 - - supp_jv_credit_amt = ( - frappe.db.get_value( - "Journal Entry Account", - { - "parent": ("in", vouchers), - "docstatus": 1, - "party": ("in", parties), - "reference_type": ("!=", "Purchase Invoice"), - }, - "sum(credit_in_account_currency - debit_in_account_currency)", - ) - or 0.0 - ) + if consider_party_ledger_amount: + pe_filters.pop("apply_tax_withholding_amount", None) + pe_filters.pop("tax_withholding_category", None) # Get Amount via payment entry - payment_entry_amounts = frappe.db.get_all( + payment_entries = frappe.db.get_all( "Payment Entry", - filters=payment_entry_filters, - fields=["sum(unallocated_amount) as amount", "payment_type"], - group_by="payment_type", + filters=pe_filters, + fields=["name", "unallocated_amount as taxable_amount", "payment_type"], ) - supp_credit_amt = supp_jv_credit_amt - supp_credit_amt += inv.get("tax_withholding_net_total", 0) - - for type in payment_entry_amounts: - if type.payment_type == "Pay": - supp_credit_amt += type.amount - else: - supp_credit_amt -= type.amount + for row in payment_entries: + value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount + pe_credit_amt += value + voucher_wise_amount.append( + frappe._dict( + { + "voucher_name": row.name, + "voucher_type": "Payment Entry", + "taxable_amount": value, + } + ) + ) threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) + supp_credit_amt = jv_credit_emt + pe_credit_amt + inv.get("tax_withholding_net_total", 0) + tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0) - if inv.doctype != "Payment Entry": - tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0) - else: - tax_withholding_net_total = inv.get("tax_withholding_net_total", 0) + # if consider_party_ledger_amount is checked, then threshold will be based on grand total + amt_for_threshold = pi_grand_total if consider_party_ledger_amount else pi_base_taxable_net_total has_cumulative_threshold_breached = ( - cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold + cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold ) if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached): - # Get net total again as TDS is calculated on net total - # Grand is used to just check for threshold breach - net_total = ( - frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0 - ) - supp_credit_amt += net_total + supp_credit_amt += pi_base_taxable_net_total if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount): - supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold + supp_credit_amt = pi_base_taxable_net_total + tax_withholding_net_total - cumulative_threshold if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0): tds_amount = get_lower_deduction_amount( diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index f2ebfc60cd7e..c4ab2f945812 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -569,6 +569,15 @@ def test_tax_withholding_category_voucher_display(self): pi1.submit() invoices.append(pi1) + pe = create_payment_entry( + payment_type="Pay", party_type="Supplier", party="Test TDS Supplier6", paid_amount=1000 + ) + pe.apply_tax_withholding_amount = 1 + pe.tax_withholding_category = "Test Multi Invoice Category" + pe.save() + pe.submit() + invoices.append(pe) + pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True) pi2.apply_tds = 1 pi2.tax_withholding_category = "Test Multi Invoice Category" @@ -584,6 +593,8 @@ def test_tax_withholding_category_voucher_display(self): self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total) self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name) self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total) + self.assertTrue(pi2.tax_withheld_vouchers[2].voucher_name == pe.name) + self.assertTrue(pi2.tax_withheld_vouchers[2].taxable_amount == pe.paid_amount) # cancel invoices to avoid clashing for d in reversed(invoices): From 8f73978a26c05357bd596db61d93013a4d9dfc63 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Sat, 25 Jan 2025 13:16:06 +0530 Subject: [PATCH 74/78] fix: variable names (cherry picked from commit d97e78e5d3523f8d7689f179595ccd279836638c) --- .../tax_withholding_category.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 9987ff5e031a..3215b93a4969 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -532,17 +532,17 @@ def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount): tds_amount = 0 pi_grand_total = 0 - pi_base_taxable_net_total = 0 - jv_credit_emt = 0 + pi_base_net_total = 0 + jv_credit_amt = 0 pe_credit_amt = 0 for row in voucher_wise_amount: if row.voucher_type == "Purchase Invoice": pi_grand_total += row.get("grand_total", 0) - pi_base_taxable_net_total += row.get("taxable_amount", 0) + pi_base_net_total += row.get("taxable_amount", 0) if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice": - jv_credit_emt += row.get("taxable_amount", 0) + jv_credit_amt += row.get("taxable_amount", 0) ## for TDS to be deducted on advances pe_filters = { @@ -556,9 +556,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount): "company": inv.company, } - consider_party_ledger_amount = cint(tax_details.consider_party_ledger_amount) + consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount) - if consider_party_ledger_amount: + if consider_party_ledger_amt: pe_filters.pop("apply_tax_withholding_amount", None) pe_filters.pop("tax_withholding_category", None) @@ -584,21 +584,21 @@ def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount): threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) - supp_credit_amt = jv_credit_emt + pe_credit_amt + inv.get("tax_withholding_net_total", 0) + supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0) tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0) # if consider_party_ledger_amount is checked, then threshold will be based on grand total - amt_for_threshold = pi_grand_total if consider_party_ledger_amount else pi_base_taxable_net_total + amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total - has_cumulative_threshold_breached = ( + cumulative_threshold_breached = ( cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold ) - if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached): - supp_credit_amt += pi_base_taxable_net_total + if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached): + supp_credit_amt += pi_base_net_total - if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount): - supp_credit_amt = pi_base_taxable_net_total + tax_withholding_net_total - cumulative_threshold + if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount): + supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0): tds_amount = get_lower_deduction_amount( From ef2f4118d953276a8a33b7dc545ff9d89ed86548 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:39:30 +0530 Subject: [PATCH 75/78] fix: get stock balance filtered by company for validating stock value in jv (backport #45549) (#45578) * fix: get stock balance filtered by company for validating stock value in jv (#45549) * fix: get stock balance filtered by company for validating stock value in jv * test: error is raised on validate (cherry picked from commit 9f20854bd9da74412cb220ce2caa2e246b9cf169) * fix: correct args for test case function --------- Co-authored-by: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: ljain112 --- .../accounts/doctype/journal_entry/test_journal_entry.py | 3 +-- .../doctype/payment_request/test_payment_request.py | 6 ++---- erpnext/accounts/utils.py | 2 +- erpnext/stock/utils.py | 8 +++++++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 8f4c4e3ccda3..54aa3eaf96f7 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -146,10 +146,9 @@ def test_jv_against_stock_account(self): "credit_in_account_currency": 0 if diff > 0 else abs(diff), }, ) - jv.insert() if account_bal == stock_bal: - self.assertRaises(StockAccountInvalidTransaction, jv.submit) + self.assertRaises(StockAccountInvalidTransaction, jv.save) frappe.db.rollback() else: jv.submit() diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index eadb714baa30..ed940470d6c0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -83,8 +83,7 @@ def test_payment_request_linkings(self): def test_payment_entry_against_purchase_invoice(self): si_usd = make_purchase_invoice( - customer="_Test Supplier USD", - debit_to="_Test Payable USD - _TC", + supplier="_Test Supplier USD", currency="USD", conversion_rate=50, ) @@ -108,8 +107,7 @@ def test_payment_entry_against_purchase_invoice(self): def test_multiple_payment_entry_against_purchase_invoice(self): purchase_invoice = make_purchase_invoice( - customer="_Test Supplier USD", - debit_to="_Test Payable USD - _TC", + supplier="_Test Supplier USD", currency="USD", conversion_rate=50, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a7f8581e0f8a..b3c82e841924 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1644,7 +1644,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) if wh_details.account == account and not wh_details.is_group ] - total_stock_value = get_stock_value_on(related_warehouses, posting_date) + total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company) precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index ee5893eb826e..6369562a62d2 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -58,7 +58,10 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): def get_stock_value_on( - warehouses: list | str | None = None, posting_date: str | None = None, item_code: str | None = None + warehouses: list | str | None = None, + posting_date: str | None = None, + item_code: str | None = None, + company: str | None = None, ) -> float: if not posting_date: posting_date = nowdate() @@ -84,6 +87,9 @@ def get_stock_value_on( if item_code: query = query.where(sle.item_code == item_code) + if company: + query = query.where(sle.company == company) + return query.run(as_list=True)[0][0] From 2d2f30e6cf4aacabe46f827f66dd67f0e6dfd1b6 Mon Sep 17 00:00:00 2001 From: Safvan Huzain <92985225+safvanhuzain@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:57:01 +0530 Subject: [PATCH 76/78] fix(query): remove duplicate docstatus condition (#45586) fix: remove duplicate docstatus condition in query (cherry picked from commit 3f2e93dcb682666036f458e3fea05804233bcbc3) --- .../doctype/bank_reconciliation_tool/bank_reconciliation_tool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 62a4c74a9330..9de1b4216ce6 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -802,7 +802,6 @@ def get_je_matching_query( .where(je.clearance_date.isnull()) .where(jea.account == common_filters.bank_account) .where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0) - .where(je.docstatus == 1) .where(filter_by_date) .orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date) ) From e3855949e14d445fe6becfd2dae91fff9309ba42 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 28 Jan 2025 17:54:16 +0530 Subject: [PATCH 77/78] fix: update voucher outstanding from payment ledger (cherry picked from commit dd7707035148b4d8a2d36c57f0f5dbc476707734) --- .../purchase_invoice/purchase_invoice.py | 15 ++++++------- .../doctype/sales_invoice/sales_invoice.py | 22 +++++++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3d3481c053db..3ab214751f77 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -10,7 +10,6 @@ import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date -from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, validate_docs_for_voucher_types, @@ -33,7 +32,7 @@ merge_similar_entries, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.accounts.utils import get_account_currency, get_fiscal_year +from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status @@ -838,12 +837,12 @@ def cancel_provisional_entries(self): def update_supplier_outstanding(self, update_outstanding): if update_outstanding == "No": - update_outstanding_amt( - self.credit_to, - "Supplier", - self.supplier, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, + update_voucher_outstanding( + voucher_type=self.doctype, + voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name, + account=self.credit_to, + party_type="Supplier", + party=self.supplier, ) def get_gl_entries(self, warehouse_account=None): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6e039b4b34fb..5753eba8cc16 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -24,7 +24,11 @@ ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import get_due_date, get_party_account, get_party_details -from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + get_account_currency, + update_voucher_outstanding, +) from erpnext.assets.doctype.asset.depreciation import ( depreciate_asset, get_disposal_account_and_cost_center, @@ -1192,14 +1196,14 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": - from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt - - update_outstanding_amt( - self.debit_to, - "Customer", - self.customer, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, + update_voucher_outstanding( + voucher_type=self.doctype, + voucher_no=self.return_against + if cint(self.is_return) and self.return_against + else self.name, + account=self.debit_to, + party_type="Customer", + party=self.customer, ) elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock): From 2b16eb53810b3a0944ae02cbec80913f036ef16a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Jan 2025 16:36:17 +0530 Subject: [PATCH 78/78] fix: do not allow to manually submit the SABB --- .../serial_and_batch_bundle/serial_and_batch_bundle.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 21f0784df75e..b35b3aa0f196 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -6,6 +6,10 @@ frappe.ui.form.on("Serial and Batch Bundle", { frm.trigger("set_queries"); }, + before_submit(frm) { + frappe.throw(__("User cannot submitted the Serial and Batch Bundle manually")); + }, + refresh(frm) { frm.trigger("toggle_fields"); frm.trigger("prepare_serial_batch_prompt");