diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index d5f00527444..e2d89573cbf 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -43,9 +43,11 @@ jobs: fi - name: Setup Python - uses: "gabrielfalcao/pyenv-action@v9" + uses: "actions/setup-python@v4" with: - versions: 3.10:latest, 3.7:latest + python-version: | + 3.7 + 3.10 - name: Setup Node uses: actions/setup-node@v2 @@ -92,7 +94,6 @@ jobs: - name: Install run: | pip install frappe-bench - pyenv global $(pyenv versions | grep '3.10') bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: DB: mariadb @@ -107,7 +108,6 @@ jobs: git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git - pyenv global $(pyenv versions | grep '3.7') for version in $(seq 12 13) do echo "Updating to v$version" @@ -120,7 +120,7 @@ jobs: git -C "apps/erpnext" checkout -q -f $branch_name rm -rf ~/frappe-bench/env - bench setup env + bench setup env --python python3.7 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext @@ -132,9 +132,8 @@ jobs: git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" - pyenv global $(pyenv versions | grep '3.10') rm -rf ~/frappe-bench/env - bench -v setup env + bench -v setup env --python python3.10 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 09482d7d301..7cd498da6bf 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -61,7 +61,10 @@ "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", - "show_balance_in_coa" + "show_balance_in_coa", + "banking_tab", + "enable_party_matching", + "enable_fuzzy_matching" ], "fields": [ { @@ -383,6 +386,26 @@ "fieldname": "show_taxes_as_table_in_print", "fieldtype": "Check", "label": "Show Taxes as Table in Print" + }, + { + "fieldname": "banking_tab", + "fieldtype": "Tab Break", + "label": "Banking" + }, + { + "default": "0", + "description": "Auto match and set the Party in Bank Transactions", + "fieldname": "enable_party_matching", + "fieldtype": "Check", + "label": "Enable Automatic Party Matching" + }, + { + "default": "0", + "depends_on": "enable_party_matching", + "description": "Approximately match the description/party name against parties", + "fieldname": "enable_fuzzy_matching", + "fieldtype": "Check", + "label": "Enable Fuzzy Matching" } ], "icon": "icon-cog", @@ -390,7 +413,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-06-13 18:47:46.430291", + "modified": "2023-06-15 16:35:45.123456", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index b91f0f91371..363a2776aa7 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -70,7 +70,6 @@ def make_bank_account(doctype, docname): return doc -@frappe.whitelist() def get_party_bank_account(party_type, party): return frappe.db.get_value(party_type, party, "default_bank_account") 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 c4a23a640c3..0eef3e9a67b 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt +from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, @@ -140,6 +141,9 @@ def create_journal_entry_bts( second_account ) ) + + company = frappe.get_value("Account", company_account, "company") + accounts = [] # Multi Currency? accounts.append( @@ -149,6 +153,7 @@ def create_journal_entry_bts( "debit_in_account_currency": bank_transaction.withdrawal, "party_type": party_type, "party": party, + "cost_center": get_default_cost_center(company), } ) @@ -158,11 +163,10 @@ def create_journal_entry_bts( "bank_account": bank_transaction.bank_account, "credit_in_account_currency": bank_transaction.withdrawal, "debit_in_account_currency": bank_transaction.deposit, + "cost_center": get_default_cost_center(company), } ) - company = frappe.get_value("Account", company_account, "company") - journal_entry_dict = { "voucher_type": entry_type, "company": company, diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py new file mode 100644 index 00000000000..5d94a08f2f0 --- /dev/null +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -0,0 +1,178 @@ +from typing import Tuple, Union + +import frappe +from frappe.utils import flt +from rapidfuzz import fuzz, process + + +class AutoMatchParty: + """ + Matches by Account/IBAN and then by Party Name/Description sequentially. + Returns when a result is obtained. + + Result (if present) is of the form: (Party Type, Party,) + """ + + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + def match(self) -> Union[Tuple, None]: + result = None + result = AutoMatchbyAccountIBAN( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + deposit=self.deposit, + ).match() + + fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching") + if not result and fuzzy_matching_enabled: + result = AutoMatchbyPartyNameDescription( + bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit + ).match() + + return result + + +class AutoMatchbyAccountIBAN: + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + 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 + + def match_account_in_party(self) -> Union[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() + + 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 party_result: + result = ( + party, + party_result[0], + ) + break + + return result + + def get_or_filters(self) -> dict: + or_filters = {} + if self.bank_party_account_number: + or_filters["bank_account_no"] = self.bank_party_account_number + + if self.bank_party_iban: + or_filters["iban"] = self.bank_party_iban + + return or_filters + + +class AutoMatchbyPartyNameDescription: + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + def match(self) -> Union[Tuple, None]: + # fuzzy search by customer/supplier & employee + if not (self.bank_party_name or self.description): + return None + + result = self.match_party_name_desc_in_party() + return result + + def match_party_name_desc_in_party(self) -> Union[Tuple, None]: + """Fuzzy search party name and/or description against parties in the system""" + result = None + parties = get_parties_in_order(self.deposit) + + for party in parties: + filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} + names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name") + + for field in ["bank_party_name", "description"]: + if not self.get(field): + continue + + result, skip = self.fuzzy_search_and_return_result(party, names, field) + if result or skip: + break + + if result or skip: + # Skip If: It was hard to distinguish between close matches and so match is None + # OR if the right match was found + break + + return result + + def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]: + skip = False + result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio) + party_name, skip = self.process_fuzzy_result(result) + + if not party_name: + return None, skip + + return ( + party, + party_name, + ), skip + + def process_fuzzy_result(self, result: Union[list, None]): + """ + If there are multiple valid close matches return None as result may be faulty. + Return the result only if one accurate match stands out. + + Returns: Result, Skip (whether or not to discontinue matching) + """ + PARTY, SCORE, CUTOFF = 0, 1, 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 + + second_result = result[1] + if first_result[SCORE] > CUTOFF: + # 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]: + return None, True + + return first_result[PARTY], True + else: + return None, False + + +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 diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 768d2f0fa45..b32022e6fd8 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -33,7 +33,11 @@ "unallocated_amount", "party_section", "party_type", - "party" + "party", + "column_break_3czf", + "bank_party_name", + "bank_party_account_number", + "bank_party_iban" ], "fields": [ { @@ -63,7 +67,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nPending\nSettled\nUnreconciled\nReconciled" + "options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled" }, { "fieldname": "bank_account", @@ -202,11 +206,30 @@ "fieldtype": "Data", "label": "Transaction Type", "length": 50 + }, + { + "fieldname": "column_break_3czf", + "fieldtype": "Column Break" + }, + { + "fieldname": "bank_party_name", + "fieldtype": "Data", + "label": "Party Name/Account Holder (Bank Statement)" + }, + { + "fieldname": "bank_party_iban", + "fieldtype": "Data", + "label": "Party IBAN (Bank Statement)" + }, + { + "fieldname": "bank_party_account_number", + "fieldtype": "Data", + "label": "Party Account No. (Bank Statement)" } ], "is_submittable": 1, "links": [], - "modified": "2022-05-29 18:36:50.475964", + "modified": "2023-06-06 13:58:12.821411", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", @@ -260,4 +283,4 @@ "states": [], "title_field": "bank_account", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index b441af96600..f82337fbd77 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -15,6 +15,9 @@ class BankTransaction(StatusUpdater): self.clear_linked_payment_entries() self.set_status() + if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"): + self.auto_set_party() + _saving_flag = False # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting @@ -146,6 +149,26 @@ class BankTransaction(StatusUpdater): payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self ) + def auto_set_party(self): + from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty + + if self.party_type and self.party: + return + + result = AutoMatchParty( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + bank_party_name=self.bank_party_name, + description=self.description, + deposit=self.deposit, + ).match() + + if result: + party_type, party = result + frappe.db.set_value( + "Bank Transaction", self.name, field={"party_type": party_type, "party": party} + ) + @frappe.whitelist() def get_doctypes_for_bank_reconciliation(): diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py new file mode 100644 index 00000000000..36ef1fca074 --- /dev/null +++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py @@ -0,0 +1,151 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate + +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account + + +class TestAutoMatchParty(FrappeTestCase): + @classmethod + def setUpClass(cls): + create_bank_account() + frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1) + frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1) + return super().setUpClass() + + @classmethod + def tearDownClass(cls): + frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0) + frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0) + + def test_match_by_account_number(self): + create_supplier_for_match(account_no="000000003716541159") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b", + account_no="000000003716541159", + iban="DE02000000003716541159", + ) + + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "John Doe & Co.") + + def test_match_by_iban(self): + create_supplier_for_match(iban="DE02000000003716541159") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="c5455a224602afaa51592a9d9250600d", + account_no="000000003716541159", + iban="DE02000000003716541159", + ) + + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "John Doe & Co.") + + def test_match_by_party_name(self): + create_supplier_for_match(supplier_name="Jackson Ella W.") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="1f6f661f347ff7b1ea588665f473adb1", + party_name="Ella Jackson", + iban="DE04000000003716545346", + ) + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "Jackson Ella W.") + + def test_match_by_description(self): + create_supplier_for_match(supplier_name="Microsoft") + doc = create_bank_transaction( + description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536", + withdrawal=1200, + transaction_id="8df880a2d09c3bed3fea358ca5168c5a", + party_name="", + ) + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "Microsoft") + + def test_skip_match_if_multiple_close_results(self): + create_supplier_for_match(supplier_name="Adithya Medical & General Stores") + create_supplier_for_match(supplier_name="Adithya Medical And General Stores") + + doc = create_bank_transaction( + description="Paracetamol Consignment, SINV-0009", + withdrawal=24.85, + transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9", + party_name="Adithya Medical & General", + ) + + # Mapping is skipped as both Supplier names have the same match score + self.assertEqual(doc.party_type, None) + self.assertEqual(doc.party, None) + + +def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None): + if frappe.db.exists("Supplier", {"supplier_name": supplier_name}): + # Update related Bank Account details + if not (iban or account_no): + return + + frappe.db.set_value( + dt="Bank Account", + dn={"party": supplier_name}, + field={"iban": iban, "bank_account_no": account_no}, + ) + return + + # Create Supplier and Bank Account for the same + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_group = "Services" + supplier.supplier_type = "Company" + supplier.insert() + + if not frappe.db.exists("Bank", "TestBank"): + bank = frappe.new_doc("Bank") + bank.bank_name = "TestBank" + bank.insert(ignore_if_duplicate=True) + + if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"): + bank_account = frappe.new_doc("Bank Account") + bank_account.account_name = supplier.name + bank_account.bank = "TestBank" + bank_account.iban = iban + bank_account.bank_account_no = account_no + bank_account.party_type = "Supplier" + bank_account.party = supplier.name + bank_account.insert() + + +def create_bank_transaction( + description=None, + withdrawal=0, + deposit=0, + transaction_id=None, + party_name=None, + account_no=None, + iban=None, +): + doc = frappe.new_doc("Bank Transaction") + doc.update( + { + "doctype": "Bank Transaction", + "description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", + "date": nowdate(), + "withdrawal": withdrawal, + "deposit": deposit, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + "transaction_id": transaction_id, + "bank_party_name": party_name, + "bank_party_account_number": account_no, + "bank_party_iban": iban, + } + ) + doc.insert() + doc.submit() + doc.reload() + + return doc diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index f51b90d8f6a..1ef5c837402 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { validate_rounding_loss: function(frm) { let allowance = frm.doc.rounding_loss_allowance; - if (!(allowance > 0 && allowance < 1)) { + if (!(allowance >= 0 && allowance < 1)) { frappe.throw(__("Rounding Loss Allowance should be between 0 and 1")); } }, diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json index 2310d1272cd..79428d591b4 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json @@ -100,15 +100,16 @@ }, { "default": "0.05", - "description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account", + "description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account", "fieldname": "rounding_loss_allowance", "fieldtype": "Float", - "label": "Rounding Loss Allowance" + "label": "Rounding Loss Allowance", + "precision": "9" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-12 21:02:09.818208", + "modified": "2023-06-20 07:29:06.972434", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation", diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 5d239c91f71..598db642f33 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document): self.set_total_gain_loss() def validate_rounding_loss_allowance(self): - if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1): + if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1): frappe.throw(_("Rounding Loss Allowance should be between 0 and 1")) def set_total_gain_loss(self): @@ -373,6 +373,24 @@ class ExchangeRateRevaluation(Document): "credit": 0, } ) + + journal_entry_accounts.append(journal_account) + + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": 0, + "credit": 0, + "debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0, + "credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"): # Base currency has balance dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit" @@ -388,22 +406,22 @@ class ExchangeRateRevaluation(Document): } ) - journal_entry_accounts.append(journal_account) + journal_entry_accounts.append(journal_account) - journal_entry_accounts.append( - { - "account": unrealized_exchange_gain_loss_account, - "balance": get_balance_on(unrealized_exchange_gain_loss_account), - "debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, - "credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0, - "debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, - "credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "exchange_rate": 1, - "reference_type": "Exchange Rate Revaluation", - "reference_name": self.name, - } - ) + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": abs(d.gain_loss) if d.gain_loss < 0 else 0, + "credit": abs(d.gain_loss) if d.gain_loss > 0 else 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_total_debit_credit() diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json index 2968359a0d0..fd2d931315c 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json @@ -73,6 +73,7 @@ "fieldname": "current_exchange_rate", "fieldtype": "Float", "label": "Current Exchange Rate", + "precision": "9", "read_only": 1 }, { @@ -92,6 +93,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "New Exchange Rate", + "precision": "9", "reqd": 1 }, { @@ -147,7 +149,7 @@ ], "istable": 1, "links": [], - "modified": "2022-12-29 19:38:52.915295", + "modified": "2023-06-22 12:39:56.446722", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation Account", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 6d9e3202f10..a51e38eefea 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 74fd5596123..83312dbd229 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -326,12 +326,10 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): - if self.voucher_type != "Depreciation Entry": - return - for d in self.get("accounts"): if ( - d.reference_type == "Asset" + self.voucher_type == "Depreciation Entry" + and d.reference_type == "Asset" and d.reference_name and d.account_type == "Depreciation" and d.debit @@ -370,6 +368,15 @@ class JournalEntry(AccountsController): else: asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit) asset.set_status() + elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name: + journal_entry_for_scrap = frappe.db.get_value( + "Asset", d.reference_name, "journal_entry_for_scrap" + ) + + if journal_entry_for_scrap == self.name: + frappe.throw( + _("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.") + ) def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9f55ba1167f..bac84db2315 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -155,6 +155,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 22836776345..89fa15172f1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -85,25 +85,29 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { - frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => { - if(enabled) { - this.frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", - "args": { - for_filter: { - company: this.frm.doc.company, - party_type: this.frm.doc.party_type, - party: this.frm.doc.party, - receivable_payable_account: this.frm.doc.receivable_payable_account + this.frm.call({ + doc: this.frm.doc, + method: 'is_auto_process_enabled', + callback: (r) => { + if (r.message) { + this.frm.call({ + 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", + "args": { + for_filter: { + company: this.frm.doc.company, + party_type: this.frm.doc.party_type, + party: this.frm.doc.party, + receivable_payable_account: this.frm.doc.receivable_payable_account + } } - } - }).then(r => { - if (r.message) { - let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); - let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); - this.frm.dashboard.add_comment(msg, "yellow"); - } - }); + }).then(r => { + if (r.message) { + let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); + let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); + this.frm.dashboard.add_comment(msg, "yellow"); + } + }); + } } }); } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2c8faecf4b1..2e4e3b0e078 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -252,6 +252,10 @@ class PaymentReconciliation(Document): return difference_amount + @frappe.whitelist() + def is_auto_process_enabled(self): + return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments") + @frappe.whitelist() def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ab7884d5209..6a558ca606b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -54,9 +54,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. hide_fields(this.frm.doc); // Show / Hide button this.show_general_ledger(); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); - if(doc.update_stock==1 && doc.docstatus==1) { + if(doc.update_stock==1) { this.show_stock_ledger(); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); } if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8cb29505eb2..68407e02210 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -88,8 +88,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } this.show_general_ledger(); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); - if(doc.update_stock) this.show_stock_ledger(); + if(doc.update_stock){ + this.show_stock_ledger(); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + } if (doc.docstatus == 1 && doc.outstanding_amount!=0 && !(cint(doc.is_return) && doc.return_against)) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 7b68dd41d93..f0d3f720948 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -320,6 +320,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_debit_note", "fieldname": "is_return", "fieldtype": "Check", "hide_days": 1, @@ -1960,6 +1961,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_return", "description": "Issue a debit note with 0 qty against an existing Sales Invoice", "fieldname": "is_debit_note", "fieldtype": "Check", @@ -2155,7 +2157,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-06-03 16:22:16.219333", + "modified": "2023-06-21 16:02:18.988799", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/shareholder/shareholder.json b/erpnext/accounts/doctype/shareholder/shareholder.json index e94aea94b75..e80b05720e0 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.json +++ b/erpnext/accounts/doctype/shareholder/shareholder.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2017-12-25 16:50:53.878430", "doctype": "DocType", @@ -111,11 +112,12 @@ "read_only": 1 } ], - "modified": "2019-11-17 23:24:11.395882", + "links": [], + "modified": "2023-04-10 22:02:20.406087", "modified_by": "Administrator", "module": "Accounts", "name": "Shareholder", - "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -158,6 +160,7 @@ "search_fields": "folio_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index d34c21348c8..924c14bdb94 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -87,7 +87,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "project": d.project, "company": d.company, "purchase_order": d.purchase_order, - "purchase_receipt": d.purchase_receipt, + "purchase_receipt": purchase_receipt, "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, @@ -241,7 +241,7 @@ def get_columns(additional_table_columns, filters): }, { "label": _("Purchase Receipt"), - "fieldname": "Purchase Receipt", + "fieldname": "purchase_receipt", "fieldtype": "Link", "options": "Purchase Receipt", "width": 100, diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0ee06e8239c..a5cb3247627 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -237,11 +237,6 @@ def get_balance_on( if not (frappe.flags.ignore_account_permission or ignore_account_permission): acc.check_permission("read") - if report_type == "Profit and Loss": - # for pl accounts, get balance within a fiscal year - cond.append( - "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date - ) # different filter for group and ledger - improved performance if acc.is_group: cond.append( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index bfef57e4947..259568a24b1 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -159,15 +159,15 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): je.flags.ignore_permissions = True je.flags.planned_depr_entry = True je.save() - if not je.meta.get_workflow(): - je.submit() d.db_set("journal_entry", je.name) - idx = cint(asset_depr_schedule_doc.finance_book_id) - row = asset.get("finance_books")[idx - 1] - row.value_after_depreciation -= d.depreciation_amount - row.db_update() + if not je.meta.get_workflow(): + je.submit() + idx = cint(asset_depr_schedule_doc.finance_book_id) + row = asset.get("finance_books")[idx - 1] + row.value_after_depreciation -= d.depreciation_amount + row.db_update() asset.db_set("depr_entry_posting_status", "Successful") diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 01fcb11d817..6d55d7772b2 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,7 +15,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } refresh() { - erpnext.hide_company(); this.show_general_ledger(); if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); @@ -129,10 +128,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } - target_asset() { - return this.get_target_asset_details(); - } - item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -247,26 +242,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } - get_target_asset_details() { - var me = this; - - if (me.frm.doc.target_asset) { - return me.frm.call({ - method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", - child: me.frm.doc, - args: { - asset: me.frm.doc.target_asset, - company: me.frm.doc.company, - }, - callback: function (r) { - if (!r.exc) { - me.frm.refresh_fields(); - } - } - }); - } - } - get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 01b35f64ab0..04b0c4e5132 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -11,13 +11,14 @@ "naming_series", "entry_type", "target_item_code", + "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset", "target_asset_name", + "target_asset_location", "target_warehouse", "target_qty", "target_stock_uom", @@ -85,14 +86,13 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", "no_copy": 1, - "options": "Asset" + "options": "Asset", + "read_only": 1 }, { "depends_on": "eval:doc.entry_type=='Capitalization'", @@ -108,11 +108,11 @@ "fieldtype": "Column Break" }, { - "fetch_from": "asset.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", + "remember_last_selected_value": 1, "reqd": 1 }, { @@ -158,7 +158,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))", "fieldname": "section_break_16", "fieldtype": "Section Break", "label": "Consumed Stock Items" @@ -189,7 +189,7 @@ "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", - "read_only_depends_on": "target_is_fixed_asset" + "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { "fetch_from": "target_item_code.stock_uom", @@ -227,7 +227,7 @@ "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", "fieldname": "section_break_26", "fieldtype": "Section Break", - "label": "Consumed Asset Items" + "label": "Consumed Assets" }, { "fieldname": "asset_items", @@ -266,7 +266,7 @@ "options": "Finance Book" }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))", "fieldname": "service_expenses_section", "fieldtype": "Section Break", "label": "Service Expenses" @@ -329,12 +329,20 @@ "label": "Target Fixed Asset Account", "options": "Account", "read_only": 1 + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "target_asset_location", + "fieldtype": "Link", + "label": "Target Asset Location", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "Location" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-10-12 15:09:40.771332", + "modified": "2023-06-22 14:17:07.995120", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 6841c56b108..a883bec71b0 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -19,9 +19,6 @@ from erpnext.assets.doctype.asset.depreciation import ( reverse_depreciation_entry_made_after_disposal, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account -from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - make_new_active_asset_depr_schedules_and_cancel_current_ones, -) from erpnext.controllers.stock_controller import StockController from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -45,7 +42,6 @@ force_fields = [ "target_has_batch_no", "target_stock_uom", "stock_uom", - "target_fixed_asset_account", "fixed_asset_account", "valuation_rate", ] @@ -56,7 +52,6 @@ class AssetCapitalization(StockController): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() - self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -71,11 +66,12 @@ class AssetCapitalization(StockController): def before_submit(self): self.validate_source_mandatory() + if self.entry_type == "Capitalization": + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() - self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ( @@ -86,7 +82,7 @@ class AssetCapitalization(StockController): ) self.update_stock_ledger() self.make_gl_entries() - self.update_target_asset() + self.restore_consumed_asset_items() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -97,15 +93,6 @@ class AssetCapitalization(StockController): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) - # Remove asset if item not a fixed asset - if not self.target_is_fixed_asset: - self.target_asset = None - - target_asset_details = get_target_asset_details(self.target_asset, self.company) - for k, v in target_asset_details.items(): - if self.meta.has_field(k) and (not self.get(k) or k in force_fields): - self.set(k, v) - for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -157,9 +144,6 @@ class AssetCapitalization(StockController): if not target_item.is_stock_item: self.target_warehouse = None - if not target_item.is_fixed_asset: - self.target_asset = None - self.target_fixed_asset_account = None if not target_item.has_batch_no: self.target_batch_no = None if not target_item.has_serial_no: @@ -170,17 +154,6 @@ class AssetCapitalization(StockController): self.validate_item(target_item) - def validate_target_asset(self): - if self.target_asset: - target_asset = self.get_asset_for_validation(self.target_asset) - - if target_asset.item_code != self.target_item_code: - frappe.throw( - _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) - ) - - self.validate_asset(target_asset) - def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -386,7 +359,11 @@ class AssetCapitalization(StockController): gl_entries, target_account, target_against, precision ) + if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable: + return [] + self.get_gl_entries_for_target_item(gl_entries, target_against, precision) + return gl_entries def get_target_account(self): @@ -429,11 +406,14 @@ class AssetCapitalization(StockController): def get_gl_entries_for_consumed_asset_items( self, gl_entries, target_account, target_against, precision ): + self.are_all_asset_items_non_depreciable = True + # Consumed Assets for item in self.asset_items: - asset = self.get_asset(item) + asset = frappe.get_doc("Asset", item.asset) if asset.calculate_depreciation: + self.are_all_asset_items_non_depreciable = False notes = _( "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." ).format( @@ -519,40 +499,46 @@ class AssetCapitalization(StockController): ) ) - def update_target_asset(self): + def create_target_asset(self): total_target_asset_value = flt(self.total_value, self.precision("total_value")) - if self.docstatus == 1 and self.entry_type == "Capitalization": - asset_doc = frappe.get_doc("Asset", self.target_asset) - asset_doc.purchase_date = self.posting_date - asset_doc.gross_purchase_amount = total_target_asset_value - asset_doc.purchase_receipt_amount = total_target_asset_value - notes = _( - "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}." - ).format( - get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) - ) - make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes) - asset_doc.flags.ignore_validate_update_after_submit = True - asset_doc.save() - elif self.docstatus == 2: - for item in self.asset_items: - asset = self.get_asset(item) - asset.db_set("disposal_date", None) - self.set_consumed_asset_status(asset) + asset_doc = frappe.new_doc("Asset") + asset_doc.company = self.company + asset_doc.item_code = self.target_item_code + asset_doc.is_existing_asset = 1 + asset_doc.location = self.target_asset_location + asset_doc.available_for_use_date = self.posting_date + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.flags.ignore_validate = True + asset_doc.insert() - if asset.calculate_depreciation: - reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) - notes = _( - "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." - ).format( - get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) - ) - reset_depreciation_schedule(asset, self.posting_date, notes) + self.target_asset = asset_doc.name - def get_asset(self, item): - asset = frappe.get_doc("Asset", item.asset) - self.check_finance_books(item, asset) - return asset + self.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=self.target_item_code, company=asset_doc.company + ) + + frappe.msgprint( + _( + "Asset {0} has been created. Please set the depreciation details if any and submit it." + ).format(get_link_to_form("Asset", asset_doc.name)) + ) + + def restore_consumed_asset_items(self): + for item in self.asset_items: + asset = frappe.get_doc("Asset", item.asset) + asset.db_set("disposal_date", None) + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) + ) + reset_depreciation_schedule(asset, self.posting_date, notes) def set_consumed_asset_status(self, asset): if self.docstatus == 1: @@ -602,33 +588,6 @@ def get_target_item_details(item_code=None, company=None): return out -@frappe.whitelist() -def get_target_asset_details(asset=None, company=None): - out = frappe._dict() - - # Get Asset Details - asset_details = frappe._dict() - if asset: - asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) - if not asset_details: - frappe.throw(_("Asset {0} does not exist").format(asset)) - - # Re-set item code from Asset - out.target_item_code = asset_details.item_code - - # Set Asset Details - out.asset_name = asset_details.asset_name - - if asset_details.item_code: - out.target_fixed_asset_account = get_asset_category_account( - "fixed_asset_account", item=asset_details.item_code, company=company - ) - else: - out.target_fixed_asset_account = None - - return out - - @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, str): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 5345d0e7f2b..6e0a6856f56 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -47,13 +47,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - TCP1", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -65,7 +58,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -94,7 +88,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) self.assertEqual(target_asset.gross_purchase_amount, total_amount) self.assertEqual(target_asset.purchase_receipt_amount, total_amount) @@ -142,13 +136,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - _TC", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -160,7 +147,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -189,7 +177,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) self.assertEqual(target_asset.gross_purchase_amount, total_amount) self.assertEqual(target_asset.purchase_receipt_amount, total_amount) @@ -364,6 +352,7 @@ def create_asset_capitalization(**args): "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), "target_item_code": target_item_code, "target_asset": target_asset.name, + "target_asset_location": "Test Location", "target_warehouse": target_warehouse, "target_qty": flt(args.target_qty) or 1, "target_batch_no": args.target_batch_no, diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 83031415ec3..641d35fa04c 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -42,7 +42,6 @@ class AssetMaintenance(Document): maintenance_log.db_set("maintenance_status", "Cancelled") -@frappe.whitelist() def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date): team_member = frappe.db.get_value("User", assign_to_member, "email") args = { diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 4f7b8361077..b788a32d6ab 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -19,56 +19,6 @@ frappe.query_reports["Fixed Asset Register"] = { options: "\nIn Location\nDisposed", default: 'In Location' }, - { - "fieldname":"filter_based_on", - "label": __("Period Based On"), - "fieldtype": "Select", - "options": ["Fiscal Year", "Date Range"], - "default": "Fiscal Year", - "reqd": 1 - }, - { - "fieldname":"from_date", - "label": __("Start Date"), - "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12), - "depends_on": "eval: doc.filter_based_on == 'Date Range'", - "reqd": 1 - }, - { - "fieldname":"to_date", - "label": __("End Date"), - "fieldtype": "Date", - "default": frappe.datetime.nowdate(), - "depends_on": "eval: doc.filter_based_on == 'Date Range'", - "reqd": 1 - }, - { - "fieldname":"from_fiscal_year", - "label": __("Start Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), - "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", - "reqd": 1 - }, - { - "fieldname":"to_fiscal_year", - "label": __("End Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), - "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", - "reqd": 1 - }, - { - "fieldname":"date_based_on", - "label": __("Date Based On"), - "fieldtype": "Select", - "options": ["Purchase Date", "Available For Use Date"], - "default": "Purchase Date", - "reqd": 1 - }, { fieldname:"asset_category", label: __("Asset Category"), @@ -89,22 +39,67 @@ frappe.query_reports["Fixed Asset Register"] = { default: "--Select a group--", reqd: 1 }, - { - fieldname:"finance_book", - label: __("Finance Book"), - fieldtype: "Link", - options: "Finance Book", - depends_on: "eval: doc.filter_by_finance_book == 1", - }, - { - fieldname:"filter_by_finance_book", - label: __("Filter by Finance Book"), - fieldtype: "Check" - }, { fieldname:"only_existing_assets", label: __("Only existing assets"), fieldtype: "Check" }, + { + fieldname:"finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + }, + { + "fieldname": "include_default_book_assets", + "label": __("Include Default Book Assets"), + "fieldtype": "Check", + "default": 1 + }, + { + "fieldname":"filter_based_on", + "label": __("Period Based On"), + "fieldtype": "Select", + "options": ["--Select a period--", "Fiscal Year", "Date Range"], + "default": "--Select a period--", + }, + { + "fieldname":"from_date", + "label": __("Start Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12), + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + }, + { + "fieldname":"to_date", + "label": __("End Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate(), + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + }, + { + "fieldname":"from_fiscal_year", + "label": __("Start Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + }, + { + "fieldname":"to_fiscal_year", + "label": __("End Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + }, + { + "fieldname":"date_based_on", + "label": __("Date Based On"), + "fieldtype": "Select", + "options": ["Purchase Date", "Available For Use Date"], + "default": "Purchase Date", + "depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'", + }, ] }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 984b3fd9820..6911f94bbbb 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -2,9 +2,11 @@ # For license information, please see license.txt +from itertools import chain + import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cstr, flt, formatdate, getdate from erpnext.accounts.report.financial_statements import ( @@ -13,7 +15,6 @@ from erpnext.accounts.report.financial_statements import ( validate_fiscal_year, ) from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation -from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts def execute(filters=None): @@ -64,11 +65,9 @@ def get_conditions(filters): def get_data(filters): - data = [] conditions = get_conditions(filters) - depreciation_amount_map = get_finance_book_value_map(filters) pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() @@ -102,20 +101,31 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) - assets_linked_to_fb = None + assets_linked_to_fb = get_assets_linked_to_fb(filters) - if filters.filter_by_finance_book: - assets_linked_to_fb = frappe.db.get_all( - doctype="Asset Finance Book", - filters={"finance_book": filters.finance_book or ("is", "not set")}, - pluck="parent", - ) + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") + + if filters.include_default_book_assets and company_fb: + finance_book = company_fb + elif filters.finance_book: + finance_book = filters.finance_book + else: + finance_book = None + + depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book) for asset in assets_record: - if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb: + if ( + assets_linked_to_fb + and asset.calculate_depreciation + and asset.asset_id not in assets_linked_to_fb + ): continue - asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) + asset_value = get_asset_value_after_depreciation( + asset.asset_id, finance_book + ) or get_asset_value_after_depreciation(asset.asset_id) + row = { "asset_id": asset.asset_id, "asset_name": asset.asset_name, @@ -126,7 +136,7 @@ def get_data(filters): or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters), + "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map), "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -140,14 +150,23 @@ def get_data(filters): def prepare_chart_data(data, filters): labels_values_map = {} - date_field = frappe.scrub(filters.date_based_on) + if filters.filter_based_on not in ("Date Range", "Fiscal Year"): + filters_filter_based_on = "Date Range" + date_field = "purchase_date" + filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field) + filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field) + else: + filters_filter_based_on = filters.filter_based_on + date_field = frappe.scrub(filters.date_based_on) + filters_from_date = filters.from_date + filters_to_date = filters.to_date period_list = get_period_list( filters.from_fiscal_year, filters.to_fiscal_year, - filters.from_date, - filters.to_date, - filters.filter_based_on, + filters_from_date, + filters_to_date, + filters_filter_based_on, "Monthly", company=filters.company, ignore_fiscal_year=True, @@ -184,59 +203,76 @@ def prepare_chart_data(data, filters): } -def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters): - if asset.calculate_depreciation: - depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0 - else: - depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) +def get_assets_linked_to_fb(filters): + afb = frappe.qb.DocType("Asset Finance Book") - return flt(depr_amount, 2) - - -def get_finance_book_value_map(filters): - date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date - - return frappe._dict( - frappe.db.sql( - """ Select - ads.asset, SUM(depreciation_amount) - FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds - WHERE - ds.parent = ads.name - AND ifnull(ads.finance_book, '')=%s - AND ads.docstatus=1 - AND ds.parentfield='depreciation_schedule' - AND ds.schedule_date<=%s - AND ds.journal_entry IS NOT NULL - GROUP BY ads.asset""", - (cstr(filters.finance_book or ""), date), - ) + query = frappe.qb.from_(afb).select( + afb.parent, ) + if filters.include_default_book_assets: + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") -def get_manual_depreciation_amount_of_asset(asset, filters): + if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'")) + + query = query.where( + (afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) + | (afb.finance_book.isnull()) + ) + else: + query = query.where( + (afb.finance_book.isin([cstr(filters.finance_book), ""])) | (afb.finance_book.isnull()) + ) + + assets_linked_to_fb = list(chain(*query.run(as_list=1))) + + return assets_linked_to_fb + + +def get_depreciation_amount_of_asset(asset, depreciation_amount_map): + return depreciation_amount_map.get(asset.asset_id) or 0.0 + + +def get_asset_depreciation_amount_map(filters, finance_book): date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date - (_, _, depreciation_expense_account) = get_depreciation_accounts(asset) - + asset = frappe.qb.DocType("Asset") gle = frappe.qb.DocType("GL Entry") + aca = frappe.qb.DocType("Asset Category Account") + company = frappe.qb.DocType("Company") - result = ( + query = ( frappe.qb.from_(gle) - .select(Sum(gle.debit)) - .where(gle.against_voucher == asset.asset_id) - .where(gle.account == depreciation_expense_account) + .join(asset) + .on(gle.against_voucher == asset.name) + .join(aca) + .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company)) + .join(company) + .on(company.name == asset.company) + .select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount")) + .where( + gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) + ) .where(gle.debit != 0) .where(gle.is_cancelled == 0) - .where(gle.posting_date <= date) - ).run() + .where(asset.docstatus == 1) + .groupby(asset.name) + ) - if result and result[0] and result[0][0]: - depr_amount = result[0][0] + if finance_book: + query = query.where( + (gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()) + ) else: - depr_amount = 0 + query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull())) - return depr_amount + if filters.filter_based_on in ("Date Range", "Fiscal Year"): + query = query.where(gle.posting_date <= date) + + asset_depr_amount_map = query.run() + + return dict(asset_depr_amount_map) def get_purchase_receipt_supplier_map(): diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 1bf7f589e23..f0097898f65 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -457,7 +457,7 @@ "link_fieldname": "party" } ], - "modified": "2023-02-18 11:05:50.592270", + "modified": "2023-05-09 15:34:13.408932", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index a8b76db0931..1967df2a26b 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -99,7 +99,6 @@ def import_string_path(path): return mod -@frappe.whitelist() def make_supplier_scorecard(source_name, target_doc=None): def update_criteria_fields(obj, target, source_parent): target.max_score, target.formula = frappe.db.get_value( diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 818c7894b7d..954668055e1 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -320,7 +320,9 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): return data[0] -def make_return_doc(doctype: str, source_name: str, target_doc=None): +def make_return_doc( + doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False +): from frappe.model.mapper import get_mapped_doc company = frappe.db.get_value("Delivery Note", source_name, "company") @@ -471,7 +473,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - if hasattr(target_doc, "stock_qty"): + if hasattr(target_doc, "stock_qty") and not return_against_rejected_qty: target_doc.stock_qty = -1 * flt( source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0) ) @@ -490,6 +492,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.purchase_receipt_item = source_doc.name + if doctype == "Purchase Receipt" and return_against_rejected_qty: + target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.rejected_qty = 0.0 + target_doc.rejected_warehouse = "" + target_doc.warehouse = source_doc.rejected_warehouse + target_doc.received_qty = target_doc.qty + elif doctype == "Purchase Invoice": returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index cdbf6c7cdba..5137e030582 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -845,6 +845,149 @@ class StockController(AccountsController): gl_entries.append(self.get_gl_dict(gl_entry, item=item)) +@frappe.whitelist() +def show_accounting_ledger_preview(company, doctype, docname): + filters = {"company": company, "include_dimensions": 1} + doc = frappe.get_doc(doctype, docname) + + gl_columns, gl_data = get_accounting_ledger_preview(doc, filters) + + frappe.db.rollback() + + return {"gl_columns": gl_columns, "gl_data": gl_data} + + +@frappe.whitelist() +def show_stock_ledger_preview(company, doctype, docname): + filters = {"company": company} + doc = frappe.get_doc(doctype, docname) + + sl_columns, sl_data = get_stock_ledger_preview(doc, filters) + + frappe.db.rollback() + + return { + "sl_columns": sl_columns, + "sl_data": sl_data, + } + + +def get_accounting_ledger_preview(doc, filters): + from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + + gl_columns, gl_data = [], [] + fields = [ + "posting_date", + "account", + "debit", + "credit", + "against", + "party", + "party_type", + "cost_center", + "against_voucher_type", + "against_voucher", + ] + + doc.docstatus = 1 + + if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"): + doc.update_stock_ledger() + + doc.make_gl_entries() + columns = get_gl_columns(filters) + gl_entries = get_gl_entries_for_preview(doc.doctype, doc.name, fields) + + gl_columns = get_columns(columns, fields) + gl_data = get_data(fields, gl_entries) + + return gl_columns, gl_data + + +def get_stock_ledger_preview(doc, filters): + from erpnext.stock.report.stock_ledger.stock_ledger import get_columns as get_sl_columns + + sl_columns, sl_data = [], [] + fields = [ + "item_code", + "stock_uom", + "actual_qty", + "qty_after_transaction", + "warehouse", + "incoming_rate", + "valuation_rate", + "stock_value", + "stock_value_difference", + ] + columns_fields = [ + "item_code", + "stock_uom", + "in_qty", + "out_qty", + "qty_after_transaction", + "warehouse", + "incoming_rate", + "in_out_rate", + "stock_value", + "stock_value_difference", + ] + + if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"): + doc.docstatus = 1 + doc.update_stock_ledger() + columns = get_sl_columns(filters) + sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields) + + sl_columns = get_columns(columns, columns_fields) + sl_data = get_data(columns_fields, sl_entries) + + return sl_columns, sl_data + + +def get_sl_entries_for_preview(doctype, docname, fields): + sl_entries = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields + ) + + for entry in sl_entries: + if entry.actual_qty > 0: + entry["in_qty"] = entry.actual_qty + entry["out_qty"] = 0 + else: + entry["out_qty"] = abs(entry.actual_qty) + entry["in_qty"] = 0 + + entry["in_out_rate"] = entry["valuation_rate"] + + return sl_entries + + +def get_gl_entries_for_preview(doctype, docname, fields): + return frappe.get_all( + "GL Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields + ) + + +def get_columns(raw_columns, fields): + return [ + {"name": d.get("label"), "editable": False, "width": 110} + for d in raw_columns + if not d.get("hidden") and d.get("fieldname") in fields + ] + + +def get_data(raw_columns, raw_data): + datatable_data = [] + for row in raw_data: + data_row = [] + for column in raw_columns: + data_row.append(row.get(column) or "") + + datatable_data.append(data_row) + + return datatable_data + + def repost_required_for_queue(doc: StockController) -> bool: """check if stock document contains repeated item-warehouse with queue based valuation. diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py index 1a3e7379281..4466c457436 100644 --- a/erpnext/e_commerce/variant_selector/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -162,6 +162,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): product_info = get_item_variant_price_dict(exact_match[0], cart_settings) if product_info: + product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item") product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock) else: product_info = None diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e57a30a88e1..61d2acefae4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -161,7 +161,6 @@ def add_account_subtype(account_subtype): frappe.throw(frappe.get_traceback()) -@frappe.whitelist() def sync_transactions(bank, bank_account): """Sync transactions based on the last integration date as the start date, after sync is completed add the transaction date of the oldest transaction as the last integration date.""" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 45a59cf7325..48986910b07 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -99,7 +99,7 @@ frappe.ui.form.on('Production Plan', { }, __('Create')); } - if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { + if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { frm.add_custom_button(__("Material Request"), ()=> { frm.trigger("make_material_request"); }, __('Create')); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0800bdd2af9..6dc1ff6a49f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -515,6 +515,9 @@ class ProductionPlan(Document): self.show_list_created_message("Work Order", wo_list) self.show_list_created_message("Purchase Order", po_list) + if not wo_list: + frappe.msgprint(_("No Work Orders were created")) + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() @@ -618,6 +621,9 @@ class ProductionPlan(Document): def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + if item.get("qty") <= 0: + return + wo = frappe.new_doc("Work Order") wo.update(item) wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 75b43ec1c30..fcfba7fca56 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -76,6 +76,13 @@ class TestProductionPlan(FrappeTestCase): "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 ) + pln.make_work_order() + nwork_orders = frappe.get_all( + "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 + ) + + self.assertTrue(len(work_orders), len(nwork_orders)) + self.assertTrue(len(work_orders), len(pln.po_items)) for name in material_requests: diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index d346357a8f8..720423b0a49 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -66,7 +66,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con } show_general_ledger() { - var me = this; + let me = this; if(this.frm.doc.docstatus > 0) { cur_frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index cc020fc2f11..4e028e4c313 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -17,6 +17,7 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; +import "./utils/ledger_preview.js" import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; diff --git a/erpnext/public/js/utils/ledger_preview.js b/erpnext/public/js/utils/ledger_preview.js new file mode 100644 index 00000000000..85d4a7d51e9 --- /dev/null +++ b/erpnext/public/js/utils/ledger_preview.js @@ -0,0 +1,78 @@ +frappe.provide('erpnext.accounts'); + +erpnext.accounts.ledger_preview = { + show_accounting_ledger_preview(frm) { + let me = this; + if(!frm.is_new() && frm.doc.docstatus == 0) { + frm.add_custom_button(__('Accounting Ledger'), function() { + frappe.call({ + "type": "GET", + "method": "erpnext.controllers.stock_controller.show_accounting_ledger_preview", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + "callback": function(response) { + me.make_dialog("Accounting Ledger Preview", "accounting_ledger_preview_html", response.message.gl_columns, response.message.gl_data); + } + }) + }, __("Preview")); + } + }, + + show_stock_ledger_preview(frm) { + let me = this + if(!frm.is_new() && frm.doc.docstatus == 0) { + frm.add_custom_button(__('Stock Ledger'), function() { + frappe.call({ + "type": "GET", + "method": "erpnext.controllers.stock_controller.show_stock_ledger_preview", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + "callback": function(response) { + me.make_dialog("Stock Ledger Preview", "stock_ledger_preview_html", response.message.sl_columns, response.message.sl_data); + } + }) + }, __("Preview")); + } + }, + + make_dialog(label, fieldname, columns, data) { + let me = this; + let dialog = new frappe.ui.Dialog({ + "size": "extra-large", + "title": __(label), + "fields": [ + { + "fieldtype": "HTML", + "fieldname": fieldname, + }, + ] + }); + + setTimeout(function() { + me.get_datatable(columns, data, dialog.get_field(fieldname).wrapper); + }, 200); + + dialog.show(); + }, + + get_datatable(columns, data, wrapper) { + const datatable_options = { + columns: columns, + data: data, + dynamicRowHeight: true, + checkboxColumn: false, + inlineFilters: true, + }; + + new frappe.DataTable( + wrapper, + datatable_options + ); + } +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index c133cd3152d..72a1594b2d2 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -568,7 +568,7 @@ "link_fieldname": "party" } ], - "modified": "2023-02-18 11:04:46.343527", + "modified": "2023-05-09 15:38:40.255193", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 99693d90918..6cb4292226c 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -78,7 +78,9 @@ "salary_mode", "bank_details_section", "bank_name", + "column_break_heye", "bank_ac_no", + "iban", "personal_details", "marital_status", "family_background", @@ -804,17 +806,26 @@ { "fieldname": "column_break_104", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_heye", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.salary_mode == 'Bank'", + "fieldname": "iban", + "fieldtype": "Data", + "label": "IBAN" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2022-09-13 10:27:14.579197", + "modified": "2023-03-30 15:57:05.174592", "modified_by": "Administrator", "module": "Setup", "name": "Employee", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 77545e0e1ad..a648195933b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -200,6 +200,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } } + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + if (doc.docstatus > 0) { this.show_stock_ledger(); if (erpnext.is_perpetual_inventory_enabled(doc.company)) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 312c166f8b7..35aad78c1a3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -121,6 +121,10 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend refresh() { var me = this; super.refresh(); + + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); //removed for temporary @@ -209,10 +213,43 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend } make_purchase_return() { - frappe.model.open_mapped_doc({ - method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", - frm: cur_frm + let me = this; + + let has_rejected_items = cur_frm.doc.items.filter((item) => { + if (item.rejected_qty > 0) { + return true; + } }) + + if (has_rejected_items && has_rejected_items.length > 0) { + frappe.prompt([ + { + label: __("Return Qty from Rejected Warehouse"), + fieldtype: "Check", + fieldname: "return_for_rejected_warehouse", + default: 1 + }, + ], function(values){ + if (values.return_for_rejected_warehouse) { + frappe.call({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return_against_rejected_warehouse", + args: { + source_name: cur_frm.doc.name + }, + callback: function(r) { + if(r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }) + } else { + cur_frm.cscript._make_purchase_return(); + } + }, __("Return Qty"), __("Make Return Entry")); + } else { + cur_frm.cscript._make_purchase_return(); + } } close_purchase_receipt() { @@ -322,6 +359,13 @@ frappe.ui.form.on('Purchase Receipt Item', { }, }); +cur_frm.cscript._make_purchase_return = function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", + frm: cur_frm + }); +} + cur_frm.cscript['Make Stock Entry'] = function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_stock_entry", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 387f0313804..0b5dc05c3ab 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1136,6 +1136,13 @@ def get_returned_qty_map(purchase_receipt): return returned_qty_map +@frappe.whitelist() +def make_purchase_return_against_rejected_warehouse(source_name): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True) + + @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ddc055656f2..19867225876 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1827,6 +1827,33 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(abs(data["stock_value_difference"]), 400.00) + def test_return_from_rejected_warehouse(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return_against_rejected_warehouse, + ) + + item_code = "_Test Item Return from Rejected Warehouse" + create_item(item_code) + + warehouse = create_warehouse("_Test Warehouse Return Qty Warehouse") + rejected_warehouse = create_warehouse("_Test Rejected Warehouse Return Qty Warehouse") + + # Step 1: Create Purchase Receipt with valuation rate 100 + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=100, + rejected_qty=2, + rejected_warehouse=rejected_warehouse, + ) + + pr_return = make_purchase_return_against_rejected_warehouse(pr.name) + self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse) + self.assertEqual(pr_return.items[0].qty, 2.0 * -1) + self.assertEqual(pr_return.items[0].rejected_qty, 0.0) + self.assertEqual(pr_return.items[0].rejected_warehouse, "") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index d5fc710625a..27066b825c1 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -13,6 +13,7 @@ from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException import erpnext +from erpnext.accounts.general_ledger import validate_accounting_period from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers from erpnext.stock.stock_ledger import ( get_affected_transactions, @@ -44,11 +45,49 @@ class RepostItemValuation(Document): self.validate_accounts_freeze() def validate_period_closing_voucher(self): + # Period Closing Voucher year_end_date = self.get_max_year_end_date(self.company) if year_end_date and getdate(self.posting_date) <= getdate(year_end_date): - msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}" + date = frappe.format(year_end_date, "Date") + msg = f"Due to period closing, you cannot repost item valuation before {date}" frappe.throw(_(msg)) + # Accounting Period + if self.voucher_type: + validate_accounting_period( + [ + frappe._dict( + { + "posting_date": self.posting_date, + "company": self.company, + "voucher_type": self.voucher_type, + } + ) + ] + ) + + # Closing Stock Balance + closing_stock = self.get_closing_stock_balance() + if closing_stock and closing_stock[0].name: + name = get_link_to_form("Closing Stock Balance", closing_stock[0].name) + to_date = frappe.format(closing_stock[0].to_date, "Date") + msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}" + frappe.throw(_(msg)) + + def get_closing_stock_balance(self): + filters = { + "company": self.company, + "status": "Completed", + "docstatus": 1, + "to_date": (">=", self.posting_date), + } + + for field in ["warehouse", "item_code"]: + if self.get(field): + filters.update({field: ("in", ["", self.get(field)])}) + + return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters) + @staticmethod def get_max_year_end_date(company): data = frappe.get_all( diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 9c4d997b316..1853f45f583 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -392,3 +392,33 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): pr.cancel() self.assertTrue(pr.docstatus == 2) self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name})) + + def test_repost_item_valuation_for_closing_stock_balance(self): + from erpnext.stock.doctype.closing_stock_balance.closing_stock_balance import ( + prepare_closing_stock_balance, + ) + + doc = frappe.new_doc("Closing Stock Balance") + doc.company = "_Test Company" + doc.from_date = today() + doc.to_date = today() + doc.submit() + + prepare_closing_stock_balance(doc.name) + doc.load_from_db() + self.assertEqual(doc.docstatus, 1) + self.assertEqual(doc.status, "Completed") + + riv = frappe.new_doc("Repost Item Valuation") + riv.update( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "based_on": "Item and Warehouse", + "posting_date": today(), + "posting_time": "00:01:00", + } + ) + + self.assertRaises(frappe.ValidationError, riv.save) + doc.cancel() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 64650bc2018..4f85ac054d0 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -191,7 +191,6 @@ def process_string_args(args): return args -@frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"]) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 402f998677d..02444064c17 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -475,7 +475,7 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) for row_idx, row in enumerate(result): for convertible_col, data in convertible_column_map.items(): - conversion_factor = conversion_factors[row.get("item_code")] or 1 + conversion_factor = conversion_factors.get(row.get("item_code")) or 1.0 for_type = data.for_type value_before_conversion = row.get(convertible_col) if for_type == "rate": diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 613c967e3d6..9beba3fd01e 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -219,7 +219,8 @@ class ItemConfigure { : '' } - ${available_qty === 0 ? '(' + __('Out of Stock') + ')' : ''} + ${available_qty === 0 && product_info && product_info?.is_stock_item + ? '(' + __('Out of Stock') + ')' : ''} @@ -236,7 +237,8 @@ class ItemConfigure { `; /* eslint-disable indent */ - if (!product_info?.allow_items_not_in_stock && available_qty === 0) { + if (!product_info?.allow_items_not_in_stock && available_qty === 0 + && product_info && product_info?.is_stock_item) { item_add_to_cart = ''; } diff --git a/pyproject.toml b/pyproject.toml index c119ada46ee..012ffb17a6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "pycountry~=22.3.5", "Unidecode~=1.3.6", "barcodenumber~=0.5.0", + "rapidfuzz~=2.15.0", # integration dependencies "gocardless-pro~=1.22.0",