diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4d2dc586779..d6a03af535f 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 6c99d29dbfc..47c4396e102 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -62,7 +62,10 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb" + "report_settings_sb", + "banking_tab", + "enable_party_matching", + "enable_fuzzy_matching" ], "fields": [ { @@ -385,6 +388,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", @@ -392,7 +415,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-06-13 18:47:46.430291", + "modified": "2023-06-15 18:47:46.430291", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", 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..bb7a4771b2d 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/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2e4e3b0e078..216d4eccac7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -347,7 +347,10 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount: + if payment_details.difference_amount and row.reference_type not in [ + "Sales Invoice", + "Purchase Invoice", + ]: self.make_difference_entry(payment_details) if entry_list: @@ -433,6 +436,8 @@ class PaymentReconciliation(Document): journal_entry.save() journal_entry.submit() + return journal_entry + def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -598,6 +603,16 @@ class PaymentReconciliation(Document): def reconcile_dr_cr_note(dr_cr_notes, company): + def get_difference_row(inv): + if inv.difference_amount != 0 and inv.difference_account: + difference_row = { + "account": inv.difference_account, + inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0, + reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0, + "cost_center": erpnext.get_default_cost_center(company), + } + return difference_row + for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -642,5 +657,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ], } ) + + if difference_entry := get_difference_row(inv): + jv.append("accounts", difference_entry) + jv.flags.ignore_mandatory = True jv.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 3be11ae31a7..2ac7df0e39b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center 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 +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item +test_dependencies = ["Item"] + class TestPaymentReconciliation(FrappeTestCase): def setUp(self): @@ -163,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase): def create_payment_reconciliation(self): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = "Customer" + pr.party_type = ( + self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" + ) pr.party = self.customer pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() @@ -890,6 +895,42 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + def test_reconciliation_purchase_invoice_against_return(self): + pi = make_purchase_invoice( + supplier="_Test Supplier USD", currency="USD", conversion_rate=50 + ).submit() + + pi_return = frappe.get_doc(pi.as_dict()) + pi_return.name = None + pi_return.docstatus = 0 + pi_return.is_return = 1 + pi_return.conversion_rate = 80 + pi_return.items[0].qty = -pi_return.items[0].qty + pi_return.submit() + + self.company = "_Test Company" + self.party_type = "Supplier" + self.customer = "_Test Supplier USD" + + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + + invoices = [] + payments = [] + for invoice in pr.invoices: + if invoice.invoice_number == pi.name: + invoices.append(invoice.as_dict()) + break + for payment in pr.payments: + if payment.reference_name == pi_return.name: + payments.append(payment.as_dict()) + break + + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index ea18adefa35..6046c13e146 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -2,7 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on('Payment Terms Template', { - setup: function(frm) { + refresh: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); + }, + allocate_payment_based_on_payment_terms: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); } }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index ea3b76c5243..7b04a68e89a 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -11,7 +11,7 @@ from frappe.utils import flt class PaymentTermsTemplate(Document): def validate(self): self.validate_invoice_portion() - self.check_duplicate_terms() + self.validate_terms() def validate_invoice_portion(self): total_portion = 0 @@ -23,9 +23,12 @@ class PaymentTermsTemplate(Document): _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" ) - def check_duplicate_terms(self): + def validate_terms(self): terms = [] for term in self.terms: + if self.allocate_payment_based_on_payment_terms and not term.payment_term: + frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx)) + term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index e6d9fe2b54d..a6c0102a7f9 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -123,22 +123,29 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - const pos_inv_promises = frm.doc.pos_transactions.map( - row => frappe.db.get_doc("POS Invoice", row.pos_invoice) - ); - - const pos_invoices = await Promise.all(pos_inv_promises); - - for (let doc of pos_invoices) { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - } - + await Promise.all([ + frappe.call({ + method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', + args: { + start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), + end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), + pos_profile: frm.doc.pos_profile, + user: frm.doc.user + }, + callback: (r) => { + let pos_invoices = r.message; + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + } + } + }) + ]) frappe.dom.unfreeze(); } }); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 03abc93e0b8..5307ccb1931 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -1,6 +1,6 @@
| {{ _(" ") }} | +{{ _(range1) }} | +{{ _(range2) }} | +{{ _(range3) }} | +{{ _(range4) }} | +{{ _(range5) }} | +{{ _(range6) }} | +{{ _("Total") }} | +
|---|---|---|---|---|---|---|---|
| {{ _("Total Outstanding") }} | ++ {{ format_number(balance_row["age"], null, 2) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }} + | +{{ _("Future Payments") }} | ++ | + | + | + | + | + | + {{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }} + | +
| {{ _("Cheques Required") }} | ++ | + | + | + | + | + | + {{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }} | +
| {{ _("Date") }} | +{{ _("Age (Days)") }} | + + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} +{{ _("Reference") }} | +{{ _("Sales Person") }} | + {% else %} +{{ _("Reference") }} | + {% endif %} + {% if not(filters.show_future_payments) %} ++ {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks") }} + {% else %} + {{ _("Party") }} + {% endif %} + | + {% endif %} +{{ _("Invoiced Amount") }} | + {% if not(filters.show_future_payments) %} +{{ _("Paid Amount") }} | ++ {% if report.report_name == "Accounts Receivable" %} + {{ _('Credit Note') }} + {% else %} + {{ _('Debit Note') }} + {% endif %} + | + {% endif %} +{{ _("Outstanding Amount") }} | + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} +{{ _("Customer LPO No.") }} | + {% endif %} +{{ _("Future Payment Ref") }} | +{{ _("Future Payment Amount") }} | +{{ _("Remaining Balance") }} | + {% endif %} + {% else %} ++ {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks")}} + {% else %} + {{ _("Party") }} + {% endif %} + | +{{ _("Total Invoiced Amount") }} | +{{ _("Total Paid Amount") }} | ++ {% if report.report_name == "Accounts Receivable Summary" %} + {{ _('Credit Note Amount') }} + {% else %} + {{ _('Debit Note Amount') }} + {% endif %} + | +{{ _("Total Outstanding Amount") }} | + {% endif %} +|||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ (data[i]["posting_date"]) }} | +{{ data[i]["age"] }} | +
+ {% if not(filters.show_future_payments) %}
+ {{ data[i]["voucher_type"] }}
+ + {% endif %} + {{ data[i]["voucher_no"] }} + |
+
+ {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+ {{ data[i]["sales_person"] }} | + {% endif %} + + {% if not (filters.show_future_payments) %} +
+ {% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ {{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} + {{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} +
+ {% if data[i]["remarks"] %}
+ {{ _("Remarks") }}:
+ {{ data[i]["remarks"] }}
+ {% endif %}
+
+ |
+ {% endif %}
+
+ + {{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }} | + + {% if not(filters.show_future_payments) %} ++ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | ++ {{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | + {% endif %} ++ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} ++ {{ data[i]["po_no"] }} | + {% endif %} +{{ data[i]["future_ref"] }} | +{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }} | + {% endif %} + {% else %} ++ {% if not(filters.show_future_payments) %} + | + {% endif %} + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} + | + {% endif %} + | + | {{ _("Total") }} | ++ {{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }} | + + {% if not(filters.show_future_payments) %} ++ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | + {% endif %} ++ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} ++ {{ data[i]["po_no"] }} | + {% endif %} +{{ data[i]["future_ref"] }} | +{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }} | + {% endif %} + {% endif %} + {% else %} + {% if(data[i]["party"] or " ") %} + {% if not(data[i]["is_total_row"]) %} +
+ {% if(not(filters.customer | filters.supplier)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ {{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} + {{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} + {{ _("Remarks") }}: + {{ data[i]["remarks"] }} + |
+ {% else %}
+ {{ _("Total") }} | + {% endif %} +{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + {% endif %} + {% endif %} ++ | + | + | + | {{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }} | + +
| 30 Days | +60 Days | +90 Days | +120 Days | +
|---|---|---|---|
| {{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }} | +
{{ _("Printed On ") }}{{ frappe.utils.now() }}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0e6118abe6b..fb60dd58724 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, @@ -1959,6 +1960,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", @@ -2153,7 +2155,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-04-28 14:15:59.901154", + "modified": "2023-06-19 16:02:05.309332", "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/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index d8827e09662..3764979bdfd 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -573,7 +573,9 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): "supplier": ("in", parties), "apply_tds": 1, "docstatus": 1, + "tax_withholding_category": ldc.tax_withholding_category, "posting_date": ("between", (ldc.valid_from, ldc.valid_upto)), + "company": ldc.company, }, "sum(tax_withholding_net_total)", ) @@ -603,7 +605,7 @@ def is_valid_certificate( ): valid = False - available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount) + available_amount = flt(certificate_limit) - flt(deducted_amount) if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0: valid = True diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index f2bf9424f72..ed3b9915591 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -284,4 +284,4 @@ {% } %} -{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
+{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
\ 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 120384ee204..6fdb2f337c0 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 @@ -92,7 +92,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, @@ -246,7 +246,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 320cf9de68c..99c4fb31b5e 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -221,11 +221,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 cc428e35cb9..fff06a6928f 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -137,15 +137,15 @@ def make_depreciation_entry(asset_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(d.finance_book_id) - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation -= d.depreciation_amount - finance_books.db_update() + if not je.meta.get_workflow(): + je.submit() + idx = cint(d.finance_book_id) + finance_books = asset.get("finance_books")[idx - 1] + finance_books.value_after_depreciation -= d.depreciation_amount + finance_books.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 9c7f70b0e57..b312f93d319 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -14,7 +14,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(); @@ -105,10 +104,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") { @@ -223,26 +218,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 d1be5752d61..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-09-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 f648823b5bf..5625fbb523b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -7,7 +7,7 @@ import frappe # import erpnext from frappe import _ -from frappe.utils import cint, flt +from frappe.utils import cint, flt, get_link_to_form from six import string_types import erpnext @@ -43,7 +43,6 @@ force_fields = [ "target_has_batch_no", "target_stock_uom", "stock_uom", - "target_fixed_asset_account", "fixed_asset_account", "valuation_rate", ] @@ -54,7 +53,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() @@ -65,17 +63,18 @@ 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 = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") 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 @@ -86,15 +85,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()) @@ -146,9 +136,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: @@ -159,17 +146,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: @@ -379,7 +355,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): @@ -422,11 +402,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 depreciate_asset(asset, self.posting_date) asset.reload() @@ -507,30 +490,41 @@ 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 - asset_doc.prepare_depreciation_data() - 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) - reset_depreciation_schedule(asset, self.posting_date) + 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) + reset_depreciation_schedule(asset, self.posting_date) def set_consumed_asset_status(self, asset): if self.docstatus == 1: @@ -580,33 +574,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, string_types): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 86861f0b165..ead7abbf340 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -39,13 +39,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, @@ -57,7 +50,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, @@ -86,7 +80,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) @@ -134,13 +128,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, @@ -152,7 +139,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, @@ -181,7 +169,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) @@ -343,6 +331,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/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index c6c9f1f98a3..8fa8f305549 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -286,7 +286,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e source_name: this.frm.doc.supplier, target: this.frm, setters: { - company: me.frm.doc.company + company: this.frm.doc.company }, get_query_filters: { docstatus: ["!=", 2], diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 66eafe9547a..3aca21ed6a6 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -457,7 +457,7 @@ "link_fieldname": "party" } ], - "modified": "2022-11-09 18:02:59.075203", + "modified": "2023-05-09 15:34:13.408932", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1ace3bf44f2..04636de6406 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -709,6 +709,7 @@ class BuyingController(SubcontractingController): "asset_quantity": row.qty if is_grouped_asset else 0, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, + "cost_center": row.cost_center, } ) 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/patches.txt b/erpnext/patches.txt index 205047602a5..390e7219522 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -334,3 +334,4 @@ erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v14_0.enable_allow_existing_serial_no +erpnext.patches.v14_0.set_report_in_process_SOA diff --git a/erpnext/patches/v14_0/set_report_in_process_SOA.py b/erpnext/patches/v14_0/set_report_in_process_SOA.py new file mode 100644 index 00000000000..9eb5e3ab9bd --- /dev/null +++ b/erpnext/patches/v14_0/set_report_in_process_SOA.py @@ -0,0 +1,10 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import frappe + + +def execute(): + process_soa = frappe.qb.DocType("Process Statement Of Accounts") + q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger") + q.run() diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7482a33653c..18bb4b9bb46 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -568,7 +568,7 @@ "link_fieldname": "party" } ], - "modified": "2022-11-08 15:52:34.462657", + "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_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 11b71c20761..9d8fe46e8ca 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -66,8 +66,7 @@ "fieldname": "driver", "fieldtype": "Link", "label": "Driver", - "options": "Driver", - "reqd": 1 + "options": "Driver" }, { "fetch_from": "driver.full_name", @@ -189,10 +188,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-30 21:21:36.610142", + "modified": "2023-06-27 11:22:27.927637", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -228,5 +228,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "driver_name" } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 1febbded52b..af2f4113e1e 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -24,6 +24,9 @@ class DeliveryTrip(Document): ) def validate(self): + if self._action == "submit" and not self.driver: + frappe.throw(_("A driver must be set to submit.")) + self.validate_stop_addresses() def on_submit(self): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7965864cd43..3ed480f88da 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -63,6 +63,11 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term + + create_payment_term("_Test Payment Term 1 for Purchase Invoice") + create_payment_term("_Test Payment Term 2 for Purchase Invoice") + if not frappe.db.exists( "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ): @@ -74,12 +79,14 @@ class TestPurchaseReceipt(FrappeTestCase): "terms": [ { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 1 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 00, }, { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 2 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 30, 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 d3bcab76ab5..5e61f0f196a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -12,6 +12,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, @@ -43,11 +44,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/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 87a23efc590..746a1cbaf17 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Warehouse", { }; }); - frm.set_query("parent_warehouse", function () { + frm.set_query("parent_warehouse", function (doc) { return { filters: { is_group: 1, diff --git a/pyproject.toml b/pyproject.toml index 5acfd392726..9b48e6b9668 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "python-stdnum~=1.16", "Unidecode~=1.2.0", "redisearch~=2.1.0", + "rapidfuzz~=2.15.0", # integration dependencies "gocardless-pro~=1.22.0",