diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 25e95bb009a..63d7c201a14 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import inspect import frappe -__version__ = "14.43.0" +__version__ = "14.45.4" def get_default_company(user=None): diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index cfe5e6e8009..8afd313322e 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -301,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False): default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension return dimension_filters, default_dimensions_map + + +def create_accounting_dimensions_for_doctype(doctype): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) 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 0eef3e9a67b..6017c5a491d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -17,6 +17,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s get_entries, ) from erpnext.accounts.utils import get_balance_on +from erpnext.setup.utils import get_exchange_rate class BankReconciliationTool(Document): @@ -129,7 +130,7 @@ def create_journal_entry_bts( bank_transaction = frappe.db.get_values( "Bank Transaction", bank_transaction_name, - fieldname=["name", "deposit", "withdrawal", "bank_account"], + fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"], as_dict=True, )[0] company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") @@ -143,29 +144,94 @@ def create_journal_entry_bts( ) company = frappe.get_value("Account", company_account, "company") + company_default_currency = frappe.get_cached_value("Company", company, "default_currency") + company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency") + second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency") + + # determine if multi-currency Journal or not + is_multi_currency = ( + True + if company_default_currency != company_account_currency + or company_default_currency != second_account_currency + or company_default_currency != bank_transaction.currency + else False + ) accounts = [] - # Multi Currency? - accounts.append( - { - "account": second_account, - "credit_in_account_currency": bank_transaction.deposit, - "debit_in_account_currency": bank_transaction.withdrawal, - "party_type": party_type, - "party": party, - "cost_center": get_default_cost_center(company), - } - ) + second_account_dict = { + "account": second_account, + "account_currency": second_account_currency, + "credit_in_account_currency": bank_transaction.deposit, + "debit_in_account_currency": bank_transaction.withdrawal, + "party_type": party_type, + "party": party, + "cost_center": get_default_cost_center(company), + } - accounts.append( - { - "account": company_account, - "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_account_dict = { + "account": company_account, + "account_currency": company_account_currency, + "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), + } + + # convert transaction amount to company currency + if is_multi_currency: + exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date) + withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal)) + deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit)) + else: + withdrawal_in_company_currency = bank_transaction.withdrawal + deposit_in_company_currency = bank_transaction.deposit + + # if second account is of foreign currency, convert and set debit and credit fields. + if second_account_currency != company_default_currency: + exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date) + second_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + "credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0, + "debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0, + } + ) + else: + second_account_dict.update( + { + "exchange_rate": 1, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + "credit_in_account_currency": deposit_in_company_currency, + "debit_in_account_currency": withdrawal_in_company_currency, + } + ) + + # if company account is of foreign currency, convert and set debit and credit fields. + if company_account_currency != company_default_currency: + exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + company_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + } + ) + else: + company_account_dict.update( + { + "exchange_rate": 1, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + "credit_in_account_currency": withdrawal_in_company_currency, + "debit_in_account_currency": deposit_in_company_currency, + } + ) + + accounts.append(second_account_dict) + accounts.append(company_account_dict) journal_entry_dict = { "voucher_type": entry_type, @@ -175,6 +241,9 @@ def create_journal_entry_bts( "cheque_no": reference_number, "mode_of_payment": mode_of_payment, } + if is_multi_currency: + journal_entry_dict.update({"multi_currency": True}) + journal_entry = frappe.new_doc("Journal Entry") journal_entry.update(journal_entry_dict) journal_entry.set("accounts", accounts) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index a70af7a90e3..db68dfad79e 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -2,6 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on("Bank Statement Import", { + onload(frm) { + frm.set_query("bank_account", function (doc) { + return { + filters: { + company: doc.company, + }, + }; + }); + }, + setup(frm) { frappe.realtime.on("data_import_refresh", ({ data_import }) => { frm.import_in_progress = false; diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 5d94a08f2f0..04dab4c28a0 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -112,7 +112,8 @@ class AutoMatchbyPartyNameDescription: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name") + field = party.lower() + "_name" + names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: if not self.get(field): @@ -131,7 +132,11 @@ class AutoMatchbyPartyNameDescription: 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) + result = process.extract( + query=self.get(field), + choices={row.get("name"): row.get("party_name") for row in names}, + scorer=fuzz.token_set_ratio, + ) party_name, skip = self.process_fuzzy_result(result) if not party_name: @@ -149,14 +154,14 @@ class AutoMatchbyPartyNameDescription: Returns: Result, Skip (whether or not to discontinue matching) """ - PARTY, SCORE, CUTOFF = 0, 1, 80 + SCORE, PARTY_ID, CUTOFF = 1, 2, 80 if not result or not len(result): return None, False first_result = result[0] if len(result) == 1: - return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True + return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True second_result = result[1] if first_result[SCORE] > CUTOFF: @@ -165,7 +170,7 @@ class AutoMatchbyPartyNameDescription: if first_result[SCORE] == second_result[SCORE]: return None, True - return first_result[PARTY], True + return first_result[PARTY_ID], True else: return None, False diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index f82337fbd77..256bde5c719 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -89,7 +89,6 @@ class BankTransaction(StatusUpdater): - 0 > a: Error: already over-allocated - clear means: set the latest transaction date as clearance date """ - gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account") remaining_amount = self.unallocated_amount for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index d61f8a6c01c..56fa6ce2f30 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -53,10 +53,18 @@ frappe.ui.form.on('Chart of Accounts Importer', { of Accounts. Please enter the account names and add more rows as per your requirement.`); } } - } + }, + { + label : "Company", + fieldname: "company", + fieldtype: "Link", + reqd: 1, + hidden: 1, + default: frm.doc.company, + }, ], primary_action: function() { - var data = d.get_values(); + let data = d.get_values(); if (!data.template_type) { frappe.throw(__('Please select Template Type to download template')); @@ -66,7 +74,8 @@ frappe.ui.form.on('Chart of Accounts Importer', { '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', { file_type: data.file_type, - template_type: data.template_type + template_type: data.template_type, + company: data.company } ); diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index e78f5f46dbc..8c2e0f87f82 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -8,6 +8,7 @@ from functools import reduce import frappe from frappe import _ +from frappe.desk.form.linked_with import get_linked_fields from frappe.model.document import Document from frappe.utils import cint, cstr from frappe.utils.csvutils import UnicodeWriter @@ -294,10 +295,8 @@ def build_response_as_excel(writer): @frappe.whitelist() -def download_template(file_type, template_type): - data = frappe._dict(frappe.local.form_dict) - - writer = get_template(template_type) +def download_template(file_type, template_type, company): + writer = get_template(template_type, company) if file_type == "CSV": # download csv file @@ -308,8 +307,7 @@ def download_template(file_type, template_type): build_response_as_excel(writer) -def get_template(template_type): - +def get_template(template_type, company): fields = [ "Account Name", "Parent Account", @@ -335,34 +333,17 @@ def get_template(template_type): ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ) else: - writer = get_sample_template(writer) + writer = get_sample_template(writer, company) return writer -def get_sample_template(writer): - template = [ - ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"], - ["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"], - ["Equity", "", "", "", 1, "", "Equity"], - ["Expenses", "", "", "", 1, "", "Expense"], - ["Income", "", "", "", 1, "", "Income"], - ["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"], - ["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"], - ["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"], - ["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"], - ["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"], - ["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"], - ["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"], - ["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"], - ["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"], - ["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"], - ["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"], - ["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"], - ] - - for row in template: - writer.writerow(row) +def get_sample_template(writer, company): + currency = frappe.db.get_value("Company", company, "default_currency") + with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f: + for row in f: + row = row.strip().split(",") + [currency] + writer.writerow(row) return writer @@ -453,14 +434,11 @@ def get_mandatory_account_types(): def unset_existing_data(company): - linked = frappe.db.sql( - '''select fieldname from tabDocField - where fieldtype="Link" and options="Account" and parent="Company"''', - as_dict=True, - ) - # remove accounts data from company - update_values = {d.fieldname: "" for d in linked} + + fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", []) + linked = [{"fieldname": name} for name in fieldnames] + update_values = {d.get("fieldname"): "" for d in linked} frappe.db.set_value("Company", company, update_values, update_values) # remove accounts data from various doctypes diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/coa_sample_template.csv b/erpnext/accounts/doctype/chart_of_accounts_importer/coa_sample_template.csv new file mode 100644 index 00000000000..85a2f2112f1 --- /dev/null +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/coa_sample_template.csv @@ -0,0 +1,17 @@ +Application Of Funds(Assets),,,,1,,Asset +Sources Of Funds(Liabilities),,,,1,,Liability +Equity,,,,1,,Equity +Expenses,,,,1,Expense Account,Expense +Income,,,,1,Income Account,Income +Bank Accounts,Application Of Funds(Assets),,,1,Bank,Asset +Cash In Hand,Application Of Funds(Assets),,,1,Cash,Asset +Stock Assets,Application Of Funds(Assets),,,1,Stock,Asset +Cost Of Goods Sold,Expenses,,,0,Cost of Goods Sold,Expense +Asset Depreciation,Expenses,,,0,Depreciation,Expense +Fixed Assets,Application Of Funds(Assets),,,0,Fixed Asset,Asset +Accounts Payable,Sources Of Funds(Liabilities),,,0,Payable,Liability +Accounts Receivable,Application Of Funds(Assets),,,1,Receivable,Asset +Stock Expenses,Expenses,,,0,Stock Adjustment,Expense +Sample Bank,Bank Accounts,,,0,Bank,Asset +Cash,Cash In Hand,,,0,Cash,Asset +Stores,Stock Assets,,,0,Stock,Asset \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d5b047389ed..d23ae877827 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -152,6 +152,12 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); + if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) { + frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() { + frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name}); + }, __('Actions')); + + } erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); }, diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 22842cec0fe..3537abf81ad 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -13,6 +13,7 @@ "party_type", "party", "due_date", + "voucher_detail_no", "cost_center", "finance_book", "voucher_type", @@ -29,7 +30,8 @@ { "fieldname": "posting_date", "fieldtype": "Date", - "label": "Posting Date" + "label": "Posting Date", + "search_index": 1 }, { "fieldname": "account_type", @@ -63,7 +65,8 @@ "fieldtype": "Link", "in_standard_filter": 1, "label": "Voucher Type", - "options": "DocType" + "options": "DocType", + "search_index": 1 }, { "fieldname": "voucher_no", @@ -71,14 +74,16 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Voucher No", - "options": "voucher_type" + "options": "voucher_type", + "search_index": 1 }, { "fieldname": "against_voucher_type", "fieldtype": "Link", "in_standard_filter": 1, "label": "Against Voucher Type", - "options": "DocType" + "options": "DocType", + "search_index": 1 }, { "fieldname": "against_voucher_no", @@ -86,7 +91,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Against Voucher No", - "options": "against_voucher_type" + "options": "against_voucher_type", + "search_index": 1 }, { "fieldname": "amount", @@ -142,12 +148,18 @@ "fieldname": "remarks", "fieldtype": "Text", "label": "Remarks" + }, + { + "fieldname": "voucher_detail_no", + "fieldtype": "Data", + "label": "Voucher Detail No", + "search_index": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-22 15:32:56.629430", + "modified": "2023-11-08 10:53:10.664896", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 6f1f34bc9f3..5cdedb73c09 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -216,6 +216,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Select Difference Account"), + size: 'extra-large', fields: [ { fieldname: "allocation", @@ -239,6 +240,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo in_list_view: 1, read_only: 1 }, { + fieldtype:'Date', + fieldname:"gain_loss_posting_date", + label: __("Posting Date"), + in_list_view: 1, + reqd: 1, + }, { + fieldtype:'Link', options: 'Account', in_list_view: 1, @@ -272,6 +280,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo args.forEach(d => { frappe.model.set_value("Payment Reconciliation Allocation", d.docname, "difference_account", d.difference_account); + frappe.model.set_value("Payment Reconciliation Allocation", d.docname, + "gain_loss_posting_date", d.gain_loss_posting_date); + }); this.reconcile_payment_entries(); @@ -287,6 +298,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo 'reference_name': d.reference_name, 'difference_amount': d.difference_amount, 'difference_account': d.difference_account, + 'gain_loss_posting_date': d.gain_loss_posting_date }); } }); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 08923e74266..d2c75111edd 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -93,6 +93,8 @@ class PaymentReconciliation(Document): "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" ) + limit = f"limit {self.payment_limit}" if self.payment_limit else " " + # nosemgrep journal_entries = frappe.db.sql( """ @@ -116,11 +118,13 @@ class PaymentReconciliation(Document): ELSE {bank_account_condition} END) order by t1.posting_date + {limit} """.format( **{ "dr_or_cr": dr_or_cr, "bank_account_condition": bank_account_condition, "condition": condition, + "limit": limit, } ), { @@ -146,7 +150,7 @@ class PaymentReconciliation(Document): if self.payment_name: conditions.append(doc.name.like(f"%{self.payment_name}%")) - self.return_invoices = ( + self.return_invoices_query = ( qb.from_(doc) .select( ConstantColumn(voucher_type).as_("voucher_type"), @@ -154,8 +158,11 @@ class PaymentReconciliation(Document): doc.return_against, ) .where(Criterion.all(conditions)) - .run(as_dict=True) ) + if self.payment_limit: + self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit) + + self.return_invoices = self.return_invoices_query.run(as_dict=True) def get_dr_or_cr_notes(self): @@ -315,6 +322,7 @@ class PaymentReconciliation(Document): res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") + res.update({"gain_loss_posting_date": pay.get("posting_date")}) if pay.get("amount") == 0: entries.append(res) @@ -421,6 +429,7 @@ class PaymentReconciliation(Document): "allocated_amount": flt(row.get("allocated_amount")), "difference_amount": flt(row.get("difference_amount")), "difference_account": row.get("difference_account"), + "difference_posting_date": row.get("gain_loss_posting_date"), "cost_center": row.get("cost_center"), } ) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 1d843abde1d..71bc498b494 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay 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.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.stock.doctype.item.test_item import create_item test_dependencies = ["Item"] @@ -85,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase): self.customer5 = make_customer("_Test PR Customer 5", "EUR") def create_account(self): - account_name = "Debtors EUR" - if not frappe.db.get_value( - "Account", filters={"account_name": account_name, "company": self.company} - ): - acc = frappe.new_doc("Account") - acc.account_name = account_name - acc.parent_account = "Accounts Receivable - _PR" - acc.company = self.company - acc.account_currency = "EUR" - acc.account_type = "Receivable" - acc.insert() - else: - name = frappe.db.get_value( - "Account", - filters={"account_name": account_name, "company": self.company}, - fieldname="name", - pluck=True, - ) - acc = frappe.get_doc("Account", name) - self.debtors_eur = acc.name + accounts = [ + { + "attribute": "debtors_eur", + "account_name": "Debtors EUR", + "parent_account": "Accounts Receivable - _PR", + "account_currency": "EUR", + "account_type": "Receivable", + }, + { + "attribute": "creditors_usd", + "account_name": "Payable USD", + "parent_account": "Accounts Payable - _PR", + "account_currency": "USD", + "account_type": "Payable", + }, + ] + + for x in accounts: + x = frappe._dict(x) + if not frappe.db.get_value( + "Account", filters={"account_name": x.account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = x.account_name + acc.parent_account = x.parent_account + acc.company = self.company + acc.account_currency = x.account_currency + acc.account_type = x.account_type + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": x.account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + setattr(self, x.attribute, acc.name) def create_sales_invoice( self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False @@ -151,6 +170,64 @@ class TestPaymentReconciliation(FrappeTestCase): payment.posting_date = posting_date return payment + def create_purchase_invoice( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + pinv = make_purchase_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.supplier, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return pinv + + def create_purchase_order( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + pord = create_purchase_order( + qty=qty, + rate=rate, + company=self.company, + customer=self.supplier, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return pord + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -163,13 +240,11 @@ class TestPaymentReconciliation(FrappeTestCase): for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() - def create_payment_reconciliation(self): + def create_payment_reconciliation(self, party_is_customer=True): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = ( - self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" - ) - pr.party = self.customer + pr.party_type = "Customer" if party_is_customer else "Supplier" + pr.party = self.customer if party_is_customer else self.supplier 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() return pr @@ -906,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase): 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() + self.supplier = "_Test Supplier USD" + pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True) + pi.supplier = self.supplier + pi.currency = "USD" + pi.conversion_rate = 50 + pi.credit_to = self.creditors_usd + pi.save().submit() pi_return = frappe.get_doc(pi.as_dict()) pi_return.name = None @@ -918,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase): 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 = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() pr.get_unreconciled_entries() invoices = [] @@ -931,6 +1011,7 @@ class TestPaymentReconciliation(FrappeTestCase): 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()) @@ -941,6 +1022,121 @@ class TestPaymentReconciliation(FrappeTestCase): # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. pr.reconcile() + def test_reconciliation_from_purchase_order_to_multiple_invoices(self): + """ + Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation + """ + + self.supplier = "_Test Supplier" + + pi1 = self.create_purchase_invoice(qty=10, rate=100) + pi2 = self.create_purchase_invoice(qty=10, rate=100) + po = self.create_purchase_order(qty=20, rate=100) + pay = get_payment_entry(po.doctype, po.name) + # Overpay Puchase Order + pay.paid_amount = 3000 + pay.save().submit() + # assert total allocated and unallocated before reconciliation + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (po.doctype, po.name, 2000), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 2) + + for x in pr.payments: + self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name)) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + # partial allocation on pi1 and full allocate on pi2 + pr.allocation[0].allocated_amount = 100 + pr.reconcile() + + # assert references and total allocated and unallocated amount + pay.reload() + self.assertEqual(len(pay.references), 3) + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (po.doctype, po.name, 900), + ) + self.assertEqual( + ( + pay.references[1].reference_doctype, + pay.references[1].reference_name, + pay.references[1].allocated_amount, + ), + (pi1.doctype, pi1.name, 100), + ) + self.assertEqual( + ( + pay.references[2].reference_doctype, + pay.references[2].reference_name, + pay.references[2].allocated_amount, + ), + (pi2.doctype, pi2.name, 1000), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + # assert references and total allocated and unallocated amount + pay.reload() + self.assertEqual(len(pay.references), 3) + # PO references should be removed now + self.assertEqual( + ( + pay.references[0].reference_doctype, + pay.references[0].reference_name, + pay.references[0].allocated_amount, + ), + (pi1.doctype, pi1.name, 100), + ) + self.assertEqual( + ( + pay.references[1].reference_doctype, + pay.references[1].reference_name, + pay.references[1].allocated_amount, + ), + (pi2.doctype, pi2.name, 1000), + ) + self.assertEqual( + ( + pay.references[2].reference_doctype, + pay.references[2].reference_name, + pay.references[2].allocated_amount, + ), + (pi1.doctype, pi1.name, 900), + ) + self.assertEqual(pay.total_allocated_amount, 2000) + self.assertEqual(pay.unallocated_amount, 1000) + self.assertEqual(pay.difference_amount, 0) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index ec718aa70d3..5b8556e7c83 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -19,6 +19,7 @@ "is_advance", "section_break_5", "difference_amount", + "gain_loss_posting_date", "column_break_7", "difference_account", "exchange_rate", @@ -151,11 +152,16 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "gain_loss_posting_date", + "fieldtype": "Date", + "label": "Difference Posting Date" } ], "istable": 1, "links": [], - "modified": "2023-09-03 07:52:33.684217", + "modified": "2023-10-23 10:44:56.066303", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 4bb18655b47..f741cd94a1e 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -185,6 +185,7 @@ "label": "Image" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -821,7 +822,7 @@ ], "istable": 1, "links": [], - "modified": "2022-11-02 12:52:39.125295", + "modified": "2023-11-14 18:33:22.585715", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", @@ -831,4 +832,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json index 1131a0fca69..b4ac9812cbf 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json @@ -110,7 +110,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-21 17:36:26.642617", + "modified": "2023-11-02 11:32:12.254018", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation Log", @@ -125,7 +125,19 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", "share": 1, "write": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 02a5114f345..643047e982d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -383,7 +383,8 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "fieldname": "column_break_15", @@ -406,7 +407,8 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "section_addresses", @@ -1592,7 +1594,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-10-16 16:24:51.886231", + "modified": "2023-11-03 15:47:30.319200", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c5e45d4f3a9..aad1eeabd79 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -33,7 +33,7 @@ from erpnext.accounts.general_ledger import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled +from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.accounts_controller import validate_account_head @@ -284,9 +284,6 @@ class PurchaseInvoice(BuyingController): # in case of auto inventory accounting, # expense account is always "Stock Received But Not Billed" for a stock item # except opening entry, drop-ship entry and fixed asset items - if item.item_code: - asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if ( auto_accounting_for_stock and item.item_code in stock_items @@ -353,22 +350,26 @@ class PurchaseInvoice(BuyingController): frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account - - elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): + elif item.is_fixed_asset and item.pr_detail: + if not asset_received_but_not_billed: + asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + item.expense_account = asset_received_but_not_billed + elif item.is_fixed_asset: + account_type = ( + "capital_work_in_progress_account" + if is_cwip_accounting_enabled(item.asset_category) + else "fixed_asset_account" + ) asset_category_account = get_asset_category_account( - "fixed_asset_account", item=item.item_code, company=self.company + account_type, item=item.item_code, company=self.company ) if not asset_category_account: - form_link = get_link_to_form("Asset Category", asset_category) + form_link = get_link_to_form("Asset Category", item.asset_category) throw( _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), title=_("Missing Account"), ) item.expense_account = asset_category_account - elif item.is_fixed_asset and item.pr_detail: - if not asset_received_but_not_billed: - asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") - item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) @@ -591,12 +592,11 @@ class PurchaseInvoice(BuyingController): def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) + if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") - self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") else: self.stock_received_but_not_billed = None - self.expenses_included_in_valuation = None self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -605,9 +605,6 @@ class PurchaseInvoice(BuyingController): self.make_item_gl_entries(gl_entries) self.make_precision_loss_gl_entry(gl_entries) - if self.check_asset_cwip_enabled(): - self.get_asset_gl_entry(gl_entries) - self.make_tax_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) @@ -711,7 +708,11 @@ class PurchaseInvoice(BuyingController): if item.item_code: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: + if ( + self.update_stock + and self.auto_accounting_for_stock + and (item.item_code in stock_items or item.is_fixed_asset) + ): # warehouse account warehouse_debit_amount = self.make_stock_adjustment_entry( gl_entries, item, voucher_wise_stock_value, account_currency @@ -826,9 +827,7 @@ class PurchaseInvoice(BuyingController): ) ) - elif not item.is_fixed_asset or ( - item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category) - ): + else: expense_account = ( item.expense_account if (not item.enable_deferred_expense or self.is_return) @@ -921,40 +920,6 @@ class PurchaseInvoice(BuyingController): ) ) - # If asset is bought through this document and not linked to PR - if self.update_stock and item.landed_cost_voucher_amount: - expenses_included_in_asset_valuation = self.get_company_default( - "expenses_included_in_asset_valuation" - ) - # Amount added through landed-cost-voucher - gl_entries.append( - self.get_gl_dict( - { - "account": expenses_included_in_asset_valuation, - "against": expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - - gl_entries.append( - self.get_gl_dict( - { - "account": expense_account, - "against": expenses_included_in_asset_valuation, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - # update gross amount of asset bought through this document assets = frappe.db.get_all( "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} @@ -979,11 +944,17 @@ class PurchaseInvoice(BuyingController): (item.purchase_receipt, valuation_tax_accounts), ) + stock_rbnb = ( + self.get_company_default("asset_received_but_not_billed") + if item.is_fixed_asset + else self.stock_received_but_not_billed + ) + if not negative_expense_booked_in_pr: gl_entries.append( self.get_gl_dict( { - "account": self.stock_received_but_not_billed, + "account": stock_rbnb, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "remarks": self.remarks or _("Accounting Entry for Stock"), @@ -998,156 +969,12 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) - def get_asset_gl_entry(self, gl_entries): - arbnb_account = None - eiiav_account = None - asset_eiiav_currency = None - - for item in self.get("items"): - if item.is_fixed_asset: - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) - base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - - item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type") - if not item.expense_account or item_exp_acc_type not in [ - "Asset Received But Not Billed", - "Fixed Asset", - ]: - if not arbnb_account: - arbnb_account = self.get_company_default("asset_received_but_not_billed") - item.expense_account = arbnb_account - - if not self.update_stock: - arbnb_currency = get_account_currency(item.expense_account) - gl_entries.append( - self.get_gl_dict( - { - "account": item.expense_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": ( - base_asset_amount if arbnb_currency == self.company_currency else asset_amount - ), - "cost_center": item.cost_center, - "project": item.project or self.project, - }, - item=item, - ) - ) - - if item.item_tax_amount: - if not eiiav_account or not asset_eiiav_currency: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - asset_eiiav_currency = get_account_currency(eiiav_account) - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "project": item.project or self.project, - "credit": item.item_tax_amount, - "credit_in_account_currency": ( - item.item_tax_amount - if asset_eiiav_currency == self.company_currency - else item.item_tax_amount / self.conversion_rate - ), - }, - item=item, - ) - ) - else: - cwip_account = get_asset_account( - "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company - ) - - cwip_account_currency = get_account_currency(cwip_account) - gl_entries.append( - self.get_gl_dict( - { - "account": cwip_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": ( - base_asset_amount if cwip_account_currency == self.company_currency else asset_amount - ), - "cost_center": self.cost_center, - "project": item.project or self.project, - }, - item=item, - ) - ) - - if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)): - if not eiiav_account or not asset_eiiav_currency: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - asset_eiiav_currency = get_account_currency(eiiav_account) - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "credit": item.item_tax_amount, - "project": item.project or self.project, - "credit_in_account_currency": ( - item.item_tax_amount - if asset_eiiav_currency == self.company_currency - else item.item_tax_amount / self.conversion_rate - ), - }, - item=item, - ) - ) - - # Assets are bought through this document then it will be linked to this document - if flt(item.landed_cost_voucher_amount): - if not eiiav_account: - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": cwip_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - - gl_entries.append( - self.get_gl_dict( - { - "account": cwip_account, - "against": eiiav_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) - - # update gross amount of assets bought through this document - assets = frappe.db.get_all( - "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} - ) - for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) - - return gl_entries + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} + ) + for asset in assets: + frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) def make_stock_adjustment_entry( self, gl_entries, item, voucher_wise_stock_value, account_currency @@ -1849,6 +1676,7 @@ def make_purchase_receipt(source_name, target_doc=None): "po_detail": "purchase_order_item", "material_request": "material_request", "material_request_item": "material_request_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 170a163b45f..dc2b37291e8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1718,9 +1718,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertTrue(return_pi.docstatus == 1) def test_gl_entries_for_standalone_debit_note(self): - make_purchase_invoice(qty=5, rate=500, update_stock=True) + from erpnext.stock.doctype.item.test_item import make_item - returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True) + item_code = make_item(properties={"is_stock_item": 1}) + make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True) + + returned_inv = make_purchase_invoice( + item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True + ) # override the rate with valuation rate sle = frappe.get_all( @@ -1730,7 +1735,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): )[0] rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) - self.assertAlmostEqual(returned_inv.items[0].rate, rate) + self.assertAlmostEqual(rate, 500) def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( @@ -1833,6 +1838,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): disable_dimension() def test_repost_accounting_entries(self): + # update repost settings + settings = frappe.get_doc("Repost Accounting Ledger Settings") + if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]: + settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True}) + settings.save() + pi = make_purchase_invoice( rate=1000, price_list_rate=1000, @@ -1858,6 +1869,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi.load_from_db() self.assertFalse(pi.repost_required) + def test_default_cost_center_for_purchase(self): + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]: + create_cost_center(cost_center_name=c_center) + + item = create_item( + "_Test Cost Center Item For Purchase", + is_stock_item=1, + buying_cost_center="_Test Cost Center Buying - _TC", + selling_cost_center="_Test Cost Center Selling - _TC", + ) + + pi = make_purchase_invoice( + item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center="" + ) + + pi.items[0].cost_center = "" + pi.set_missing_values() + pi.calculate_taxes_and_totals() + pi.save() + + self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") + def check_gl_entries( doc, diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index c7357360ec0..7f71bd61e35 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -156,6 +156,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -890,7 +891,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-03 21:01:01.824892", + "modified": "2023-11-14 18:33:48.547297", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js index 3a87a380d19..c7b7a148cfb 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js @@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", { setup: function(frm) { frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { return { - filters: { - name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']], - } + query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types" } } diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index dbb0971fdea..69cfe9fcd74 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -10,9 +10,12 @@ from frappe.utils.data import comma_and class RepostAccountingLedger(Document): def __init__(self, *args, **kwargs): super(RepostAccountingLedger, self).__init__(*args, **kwargs) - self._allowed_types = set( - ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"] - ) + self._allowed_types = [ + x.document_type + for x in frappe.db.get_all( + "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] + ) + ] def validate(self): self.validate_vouchers() @@ -157,7 +160,7 @@ def start_repost(account_repost_doc=str) -> None: doc.docstatus = 1 doc.make_gl_entries() - elif doc.doctype in ["Payment Entry", "Journal Entry"]: + elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]: if not repost_doc.delete_cancelled_entries: doc.make_gl_entries(1) doc.make_gl_entries() @@ -186,3 +189,18 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) ) ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters): + filters = {"allowed": True} + + if txt: + filters.update({"document_type": ("like", f"%{txt}%")}) + + if allowed_types := frappe.db.get_all( + "Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1 + ): + return allowed_types + return [] diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index 0e75dd2e3e1..dda0ec778f6 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): self.create_company() self.create_customer() self.create_item() + self.update_repost_settings() def teadDown(self): frappe.db.rollback() + def update_repost_settings(self): + allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") + for x in allowed_types: + repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) + repost_settings.save() + def test_01_basic_functions(self): si = create_sales_invoice( item=self.item, diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js new file mode 100644 index 00000000000..8c83ca50431 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Repost Accounting Ledger Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json new file mode 100644 index 00000000000..8aa0a840c7e --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2023-11-07 09:57:20.619939", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "allowed_types" + ], + "fields": [ + { + "fieldname": "allowed_types", + "fieldtype": "Table", + "label": "Allowed Doctypes", + "options": "Repost Allowed Types" + } + ], + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2023-11-07 14:24:13.321522", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "System Manager", + "select": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py new file mode 100644 index 00000000000..2b8230df86f --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAccountingLedgerSettings(Document): + pass diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py new file mode 100644 index 00000000000..ec4e87ffc00 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/test_repost_accounting_ledger_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRepostAccountingLedgerSettings(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/repost_allowed_types/__init__.py b/erpnext/accounts/doctype/repost_allowed_types/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json new file mode 100644 index 00000000000..ede12fbc185 --- /dev/null +++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-11-07 09:58:03.595382", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_sfzb", + "allowed" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Doctype", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "allowed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allowed" + }, + { + "fieldname": "column_break_sfzb", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-11-07 10:01:39.217861", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Allowed Types", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py new file mode 100644 index 00000000000..0e4883b0c93 --- /dev/null +++ b/erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAllowedTypes(Document): + pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a411889fbdd..75b87421dd7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -181,7 +181,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } - make_maintenance_schedule() { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule", @@ -557,15 +556,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { } } -// Income Account in Details Table -// -------------------------------- -cur_frm.set_query("income_account", "items", function(doc) { - return{ - query: "erpnext.controllers.queries.get_income_account", - filters: {'company': doc.company} - } -}); - // Cost Center in Details Table // ----------------------------- cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) { @@ -660,6 +650,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("income_account", "items", function() { + return{ + query: "erpnext.controllers.queries.get_income_account", + filters: { + 'company': frm.doc.company, + "disabled": 0 + } + } + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Return / Credit Note', diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5fc711d893b..5ec612f6b0f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -26,6 +26,7 @@ "is_return", "return_against", "update_billed_amount_in_sales_order", + "update_billed_amount_in_delivery_note", "is_debit_note", "amended_from", "accounting_dimensions_section", @@ -2144,6 +2145,13 @@ "fieldname": "use_company_roundoff_cost_center", "fieldtype": "Check", "label": "Use Company default Cost Center for Round off" + }, + { + "default": "0", + "depends_on": "eval: doc.is_return", + "fieldname": "update_billed_amount_in_delivery_note", + "fieldtype": "Check", + "label": "Update Billed Amount in Delivery Note" } ], "icon": "fa fa-file-text", @@ -2156,7 +2164,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-06-19 16:02:05.309332", + "modified": "2023-11-03 14:39:38.012346", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7f124f541f3..339c5c2778b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -261,6 +261,7 @@ class SalesInvoice(SellingController): self.update_status_updater_args() self.update_prevdoc_status() + self.update_billing_status_in_dn() self.clear_unallocated_mode_of_payments() @@ -1036,7 +1037,7 @@ class SalesInvoice(SellingController): def make_customer_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total - # because rounded_total had value even before introcution of posting GLE based on rounded total + # because rounded_total had value even before introduction of posting GLE based on rounded total grand_total = ( self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total ) @@ -1271,7 +1272,7 @@ class SalesInvoice(SellingController): if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: payment_mode.base_amount -= flt(self.change_amount) - if payment_mode.amount: + if payment_mode.base_amount: # POS, make payment entries gl_entries.append( self.get_gl_dict( @@ -1433,6 +1434,8 @@ class SalesInvoice(SellingController): ) def update_billing_status_in_dn(self, update_modified=True): + if self.is_return and not self.update_billed_amount_in_delivery_note: + return updated_delivery_notes = [] for d in self.get("items"): if d.dn_detail: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 272382e8c18..79363dbeb1b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2497,12 +2497,6 @@ class TestSalesInvoice(FrappeTestCase): "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1", ) - frappe.db.set_value( - "Company", - "_Test Company 1", - "expenses_included_in_valuation", - "Expenses Included In Valuation - _TC1", - ) # begin test si = create_sales_invoice( @@ -2540,7 +2534,7 @@ class TestSalesInvoice(FrappeTestCase): # tear down frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock) def test_sle_for_target_warehouse(self): se = make_stock_entry( @@ -2552,6 +2546,7 @@ class TestSalesInvoice(FrappeTestCase): ) si = frappe.copy_doc(test_records[0]) + si.customer = "_Test Internal Customer 3" si.update_stock = 1 si.set_warehouse = "Finished Goods - _TC" si.set_target_warehouse = "Stores - _TC" @@ -3579,6 +3574,20 @@ def create_internal_parties(): allowed_to_interact_with="_Test Company with perpetual inventory", ) + create_internal_customer( + customer_name="_Test Internal Customer 3", + represents_company="_Test Company", + allowed_to_interact_with="_Test Company", + ) + + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account) + create_internal_supplier( supplier_name="_Test Internal Supplier", represents_company="Wind Power LLC", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index f25280f007c..87cf1ae80d6 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -167,6 +167,7 @@ "print_hide": 1 }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -891,7 +892,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-25 11:58:10.723833", + "modified": "2023-11-14 18:34:10.479329", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -901,4 +902,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index 1a9066470a5..4b351f9d764 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -18,6 +18,14 @@ frappe.ui.form.on('Subscription', { } }; }); + + frm.set_query('sales_tax_template', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); }, refresh: function(frm) { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index ed6d9dbe026..07cc5dd734f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -41,7 +41,7 @@ def make_gl_entries( from_repost=from_repost, ) save_entries(gl_map, adv_adj, update_outstanding, from_repost) - # Post GL Map proccess there may no be any GL Entries + # Post GL Map process there may no be any GL Entries elif gl_map: frappe.throw( _( diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0c51e727c92..7903beec7cf 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -5,13 +5,8 @@ from typing import Optional import frappe -from frappe import _, msgprint, scrub -from frappe.contacts.doctype.address.address import ( - get_address_display, - get_company_address, - get_default_address, -) -from frappe.contacts.doctype.contact.contact import get_contact_details +from frappe import _, msgprint, qb, scrub +from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import Abs, Date, Sum @@ -133,6 +128,7 @@ def _get_party_details( party_address, company_address, shipping_address, + ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) @@ -193,6 +189,8 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + *, + ignore_permissions=False ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -205,13 +203,17 @@ def set_address_details( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) ) # address display - party_details.address_display = get_address_display(party_details[billing_address_field]) + party_details.address_display = render_address( + party_details[billing_address_field], check_permissions=not ignore_permissions + ) # shipping address if party_type in ["Customer", "Lead"]: party_details.shipping_address_name = shipping_address or get_party_shipping_address( party_type, party.name ) - party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) + party_details.shipping_address = render_address( + party_details["shipping_address_name"], check_permissions=not ignore_permissions + ) if doctype: party_details.update( get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) @@ -229,7 +231,7 @@ def set_address_details( if shipping_address: party_details.update( shipping_address=shipping_address, - shipping_address_display=get_address_display(shipping_address), + shipping_address_display=render_address(shipping_address), **get_fetch_values(doctype, "shipping_address", shipping_address) ) @@ -238,7 +240,8 @@ def set_address_details( party_details.update( billing_address=party_details.company_address, billing_address_display=( - party_details.company_address_display or get_address_display(party_details.company_address) + party_details.company_address_display + or render_address(party_details.company_address, check_permissions=False) ), **get_fetch_values(doctype, "billing_address", party_details.company_address) ) @@ -290,7 +293,34 @@ def set_contact_details(party_details, party, party_type): } ) else: - party_details.update(get_contact_details(party_details.contact_person)) + fields = [ + "name as contact_person", + "salutation", + "first_name", + "last_name", + "email_id as contact_email", + "mobile_no as contact_mobile", + "phone as contact_phone", + "designation as contact_designation", + "department as contact_department", + ] + + contact_details = frappe.db.get_value( + "Contact", party_details.contact_person, fields, as_dict=True + ) + + contact_details.contact_display = " ".join( + filter( + None, + [ + contact_details.get("salutation"), + contact_details.get("first_name"), + contact_details.get("last_name"), + ], + ) + ) + + party_details.update(contact_details) def set_other_values(party_details, party, party_type): @@ -429,11 +459,19 @@ def get_party_account_currency(party_type, party, company): def get_party_gle_currency(party_type, party, company): def generator(): - existing_gle_currency = frappe.db.sql( - """select account_currency from `tabGL Entry` - where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", - {"company": company, "party_type": party_type, "party": party}, + gl = qb.DocType("GL Entry") + existing_gle_currency = ( + qb.from_(gl) + .select(gl.account_currency) + .where( + (gl.docstatus == 1) + & (gl.company == company) + & (gl.party_type == party_type) + & (gl.party == party) + & (gl.is_cancelled == 0) + ) + .limit(1) + .run() ) return existing_gle_currency[0][0] if existing_gle_currency else None @@ -957,3 +995,13 @@ def add_party_account(party_type, party, company, account): doc.append("accounts", accounts) doc.save() + + +def render_address(address, check_permissions=True): + try: + from frappe.contacts.doctype.address.address import render_address as _render + except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render + + return frappe.call(_render, address, check_permissions=check_permissions) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 9c73cbb344f..0979cffbf3c 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -143,7 +143,18 @@ frappe.query_reports["Accounts Payable"] = { "fieldname": "show_future_payments", "label": __("Show Future Payments"), "fieldtype": "Check", + }, + { + "fieldname": "for_revaluation_journals", + "label": __("Revaluation Journals"), + "fieldtype": "Check", + }, + { + "fieldname": "ignore_accounts", + "label": __("Group by Voucher"), + "fieldtype": "Check", } + ], "formatter": function(value, row, column, data, default_formatter) { @@ -175,4 +186,4 @@ function get_party_type_options() { }); }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 9e575e669d2..0f206b1cf42 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -110,6 +110,11 @@ frappe.query_reports["Accounts Payable Summary"] = { "fieldname":"based_on_payment_terms", "label": __("Based On Payment Terms"), "fieldtype": "Check", + }, + { + "fieldname": "for_revaluation_journals", + "label": __("Revaluation Journals"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 1073be0bdc4..d6e3098e171 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -114,10 +114,13 @@ frappe.query_reports["Accounts Receivable"] = { "reqd": 1 }, { - "fieldname": "customer_group", + "fieldname":"customer_group", "label": __("Customer Group"), - "fieldtype": "Link", - "options": "Customer Group" + "fieldtype": "MultiSelectList", + "options": "Customer Group", + get_data: function(txt) { + return frappe.db.get_link_options('Customer Group', txt); + } }, { "fieldname": "payment_terms_template", @@ -172,7 +175,19 @@ frappe.query_reports["Accounts Receivable"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check", + }, + { + "fieldname": "for_revaluation_journals", + "label": __("Revaluation Journals"), + "fieldtype": "Check", + }, + { + "fieldname": "ignore_accounts", + "label": __("Group by Voucher"), + "fieldtype": "Check", } + + ], "formatter": function(value, row, column, data, default_formatter) { @@ -205,4 +220,4 @@ function get_party_type_options() { }); }); return options; -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b9c7a0bfb87..c077d829939 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -116,7 +116,12 @@ class ReceivablePayableReport(object): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: # get the balance object for voucher_type - key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) + + if self.filters.get("ignore_accounts"): + key = (ple.voucher_type, ple.voucher_no, ple.party) + else: + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) + if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, @@ -183,7 +188,10 @@ class ReceivablePayableReport(object): ): return - key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) + if self.filters.get("ignore_accounts"): + key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + else: + key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) # If payment is made against credit note # and credit note is made against a Sales Invoice @@ -192,13 +200,19 @@ class ReceivablePayableReport(object): if ple.against_voucher_no in self.return_entries: return_against = self.return_entries.get(ple.against_voucher_no) if return_against: - key = (ple.account, ple.against_voucher_type, return_against, ple.party) + if self.filters.get("ignore_accounts"): + key = (ple.against_voucher_type, return_against, ple.party) + else: + key = (ple.account, ple.against_voucher_type, return_against, ple.party) row = self.voucher_balance.get(key) if not row: # no invoice, this is an invoice / stand-alone payment / credit note - row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) + if self.filters.get("ignore_accounts"): + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + else: + row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row.party_type = ple.party_type return row @@ -267,11 +281,20 @@ class ReceivablePayableReport(object): row.invoice_grand_total = row.invoiced - if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( - (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) - or (row.voucher_no in self.err_journals) - ): + must_consider = False + if self.filters.get("for_revaluation_journals"): + if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or ( + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + ): + must_consider = True + else: + if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + or (row.voucher_no in self.err_journals) + ): + must_consider = True + if must_consider: # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -718,6 +741,7 @@ class ReceivablePayableReport(object): query = ( qb.from_(ple) .select( + ple.name, ple.account, ple.voucher_type, ple.voucher_no, @@ -731,13 +755,15 @@ class ReceivablePayableReport(object): ple.account_currency, ple.amount, ple.amount_in_account_currency, - ple.remarks, ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) .where(Criterion.any(self.or_filters)) ) + if self.filters.get("show_remarks"): + query = query.select(ple.remarks) + if self.filters.get("group_by_party"): query = query.orderby(self.ple.party, self.ple.posting_date) else: @@ -823,7 +849,13 @@ class ReceivablePayableReport(object): self.customer = qb.DocType("Customer") if self.filters.get("customer_group"): - self.get_hierarchical_filters("Customer Group", "customer_group") + groups = get_customer_group_with_children(self.filters.customer_group) + customers = ( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer["customer_group"].isin(groups)) + ) + self.qb_selection_filter.append(self.ple.party.isin(customers)) if self.filters.get("territory"): self.get_hierarchical_filters("Territory", "territory") @@ -1115,3 +1147,19 @@ class ReceivablePayableReport(object): .run() ) self.err_journals = [x[0] for x in results] if results else [] + + +def get_customer_group_with_children(customer_groups): + if not isinstance(customer_groups, list): + customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d] + + all_customer_groups = [] + for d in customer_groups: + if frappe.db.exists("Customer Group", d): + lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"]) + children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) + all_customer_groups += [c.name for c in children] + else: + frappe.throw(_("Customer Group: {0} does not exist").format(d)) + + return list(set(all_customer_groups)) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index cbeb6d3106d..f83285a1a72 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -475,6 +475,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): report = execute(filters)[1] self.assertEqual(len(report), 0) + def test_multi_customer_group_filter(self): + si = self.create_sales_invoice() + cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") + # Create a list of customer groups, e.g., ["Group1", "Group2"] + cus_groups_list = [cus_group, "_Test Customer Group 1"] + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer_group": cus_groups_list, # Use the list of customer groups + } + report = execute(filters)[1] + + # Assert that the report contains data for the specified customer groups + self.assertTrue(len(report) > 0) + + for row in report: + # Assert that the customer group of each row is in the list of customer groups + self.assertIn(row.customer_group, cus_groups_list) + def test_party_account_filter(self): si1 = self.create_sales_invoice() self.customer2 = ( diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 5ad10c7890a..2f6d2582b33 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -139,6 +139,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Show GL Balance"), "fieldtype": "Check", }, + { + "fieldname": "for_revaluation_journals", + "label": __("Revaluation Journals"), + "fieldtype": "Check", + } ], onload: function(report) { diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js index 126cd037955..12b94347e00 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js @@ -31,6 +31,18 @@ frappe.query_reports["Asset Depreciation Ledger"] = { "fieldtype": "Link", "options": "Asset" }, + { + "fieldname":"asset_category", + "label": __("Asset Category"), + "fieldtype": "Link", + "options": "Asset Category" + }, + { + "fieldname":"cost_center", + "label": __("Cost Center"), + "fieldtype": "Link", + "options": "Cost Center" + }, { "fieldname":"finance_book", "label": __("Finance Book"), @@ -38,10 +50,10 @@ frappe.query_reports["Asset Depreciation Ledger"] = { "options": "Finance Book" }, { - "fieldname":"asset_category", - "label": __("Asset Category"), - "fieldtype": "Link", - "options": "Asset Category" - } + "fieldname": "include_default_book_assets", + "label": __("Include Default FB Assets"), + "fieldtype": "Check", + "default": 1 + }, ] } diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json index 0ef9d858dd5..9002e23ed33 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json @@ -1,15 +1,15 @@ { - "add_total_row": 1, + "add_total_row": 0, "columns": [], "creation": "2016-04-08 14:49:58.133098", "disabled": 0, "docstatus": 0, "doctype": "Report", "filters": [], - "idx": 2, + "idx": 6, "is_standard": "Yes", "letterhead": null, - "modified": "2023-07-26 21:05:33.554778", + "modified": "2023-11-08 20:17:05.774211", "modified_by": "Administrator", "module": "Accounts", "name": "Asset Depreciation Ledger", diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index f21c94b4940..d285f28d8e3 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import flt +from frappe.utils import cstr, flt def execute(filters=None): @@ -32,7 +32,6 @@ def get_data(filters): filters_data.append(["against_voucher", "=", filters.get("asset")]) if filters.get("asset_category"): - assets = frappe.db.sql_list( """select name from tabAsset where asset_category = %s and docstatus=1""", @@ -41,12 +40,27 @@ def get_data(filters): filters_data.append(["against_voucher", "in", assets]) - if filters.get("finance_book"): - filters_data.append(["finance_book", "in", ["", filters.get("finance_book")]]) + company_fb = frappe.get_cached_value("Company", filters.get("company"), "default_finance_book") + + if filters.get("include_default_book_assets") and company_fb: + if filters.get("finance_book") and cstr(filters.get("finance_book")) != cstr(company_fb): + frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'")) + else: + finance_book = company_fb + elif filters.get("finance_book"): + finance_book = filters.get("finance_book") + else: + finance_book = None + + if finance_book: + or_filters_data = [["finance_book", "in", ["", finance_book]], ["finance_book", "is", "not set"]] + else: + or_filters_data = [["finance_book", "in", [""]], ["finance_book", "is", "not set"]] gl_entries = frappe.get_all( "GL Entry", filters=filters_data, + or_filters=or_filters_data, fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], order_by="against_voucher, posting_date", ) @@ -61,7 +75,9 @@ def get_data(filters): asset_data = assets_details.get(d.against_voucher) if asset_data: if not asset_data.get("accumulated_depreciation_amount"): - asset_data.accumulated_depreciation_amount = d.debit + asset_data.accumulated_depreciation_amount = d.debit + asset_data.get( + "opening_accumulated_depreciation" + ) else: asset_data.accumulated_depreciation_amount += d.debit @@ -70,7 +86,7 @@ def get_data(filters): { "depreciation_amount": d.debit, "depreciation_date": d.posting_date, - "amount_after_depreciation": ( + "value_after_depreciation": ( flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount) ), "depreciation_entry": d.voucher_no, @@ -88,10 +104,12 @@ def get_assets_details(assets): fields = [ "name as asset", "gross_purchase_amount", + "opening_accumulated_depreciation", "asset_category", "status", "depreciation_method", "purchase_date", + "cost_center", ] for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}): @@ -121,6 +139,12 @@ def get_columns(): "fieldtype": "Currency", "width": 120, }, + { + "label": _("Opening Accumulated Depreciation"), + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "width": 140, + }, { "label": _("Depreciation Amount"), "fieldname": "depreciation_amount", @@ -134,8 +158,8 @@ def get_columns(): "width": 210, }, { - "label": _("Amount After Depreciation"), - "fieldname": "amount_after_depreciation", + "label": _("Value After Depreciation"), + "fieldname": "value_after_depreciation", "fieldtype": "Currency", "width": 180, }, @@ -153,12 +177,13 @@ def get_columns(): "options": "Asset Category", "width": 120, }, - {"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, { - "label": _("Depreciation Method"), - "fieldname": "depreciation_method", - "fieldtype": "Data", - "width": 130, + "label": _("Cost Center"), + "fieldtype": "Link", + "fieldname": "cost_center", + "options": "Cost Center", + "width": 100, }, + {"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, {"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120}, ] diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index 4a4ad4d71cb..f1f8e5f6e7c 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -15,7 +15,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Balance Sheet"]["filters"].push({ "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), + "label": __("Include Default FB Entries"), "fieldtype": "Check", "default": 1 }); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index a2c34c6ee26..b9c62b52861 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -16,7 +16,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Cash Flow"]["filters"].push( { "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), + "label": __("Include Default FB Entries"), "fieldtype": "Check", "default": 1 } diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index b165c88c068..24e585e07f6 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -67,7 +67,7 @@ def setup_mappers(mappers): mapping["finance_costs"] = [] mapping["finance_costs_adjustments"] = [] doc = frappe.get_doc("Cash Flow Mapper", mapping["name"]) - mapping_names = [item.name for item in doc.accounts] + mapping_names = [item.mapping for item in doc.accounts] if not mapping_names: continue diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index d58fd95a840..b7d25c41982 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -105,7 +105,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }, { "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), + "label": __("Include Default FB Entries"), "fieldtype": "Check", "default": 1 }, diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 693725d8f50..096bb107069 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -561,9 +561,7 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") 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 Entries'") - ) + frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) query = query.where( (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 099884a48ec..696a03b0a70 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -79,7 +79,9 @@ class General_Payment_Ledger_Comparison(object): .select( gle.company, gle.account, + gle.voucher_type, gle.voucher_no, + gle.party_type, gle.party, outstanding, ) @@ -89,7 +91,9 @@ class General_Payment_Ledger_Comparison(object): & (gle.account.isin(val.accounts)) ) .where(Criterion.all(filter_criterion)) - .groupby(gle.company, gle.account, gle.voucher_no, gle.party) + .groupby( + gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party + ) .run() ) @@ -112,7 +116,13 @@ class General_Payment_Ledger_Comparison(object): self.account_types[acc_type].ple = ( qb.from_(ple) .select( - ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding") + ple.company, + ple.account, + ple.voucher_type, + ple.voucher_no, + ple.party_type, + ple.party, + Sum(ple.amount).as_("outstanding"), ) .where( (ple.company == self.filters.company) @@ -120,7 +130,9 @@ class General_Payment_Ledger_Comparison(object): & (ple.account.isin(val.accounts)) ) .where(Criterion.all(filter_criterion)) - .groupby(ple.company, ple.account, ple.voucher_no, ple.party) + .groupby( + ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party + ) .run() ) @@ -138,12 +150,12 @@ class General_Payment_Ledger_Comparison(object): self.diff = frappe._dict({}) for x in self.variation_in_payment_ledger: - self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) + self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]}) for x in self.variation_in_general_ledger: - self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update( - frappe._dict({"pl_balance": x[4]}) - ) + self.diff.setdefault( + (x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0}) + ).update(frappe._dict({"pl_balance": x[6]})) def generate_data(self): self.data = [] @@ -151,8 +163,12 @@ class General_Payment_Ledger_Comparison(object): self.data.append( frappe._dict( { - "voucher_no": key[2], - "party": key[3], + "company": key[0], + "account": key[1], + "voucher_type": key[2], + "voucher_no": key[3], + "party_type": key[4], + "party": key[5], "gl_balance": val.gl_balance, "pl_balance": val.pl_balance, } @@ -162,12 +178,52 @@ class General_Payment_Ledger_Comparison(object): def get_columns(self): self.columns = [] options = None + self.columns.append( + dict( + label=_("Company"), + fieldname="company", + fieldtype="Link", + options="Company", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Account"), + fieldname="account", + fieldtype="Link", + options="Account", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Voucher Type"), + fieldname="voucher_type", + fieldtype="Link", + options="DocType", + width="100", + ) + ) + self.columns.append( dict( label=_("Voucher No"), fieldname="voucher_no", - fieldtype="Data", - options=options, + fieldtype="Dynamic Link", + options="voucher_type", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Party Type"), + fieldname="party_type", + fieldtype="Link", + options="DocType", width="100", ) ) @@ -176,8 +232,8 @@ class General_Payment_Ledger_Comparison(object): dict( label=_("Party"), fieldname="party", - fieldtype="Data", - options=options, + fieldtype="Dynamic Link", + options="party_type", width="100", ) ) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py index 4b0e99d7125..59e906ba332 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -50,7 +50,11 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin): self.assertEqual(len(data), 1) expected = { + "company": sinv.company, + "account": sinv.debit_to, + "voucher_type": sinv.doctype, "voucher_no": sinv.name, + "party_type": "Customer", "party": sinv.customer, "gl_balance": sinv.grand_total, "pl_balance": sinv.grand_total - 1, diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 57a9091cf9b..f0ac3d0ffdb 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -175,7 +175,7 @@ frappe.query_reports["General Ledger"] = { }, { "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), + "label": __("Include Default FB Entries"), "fieldtype": "Check", "default": 1 }, @@ -188,7 +188,13 @@ frappe.query_reports["General Ledger"] = { "fieldname": "show_net_values_in_party_account", "label": __("Show Net Values in Party Account"), "fieldtype": "Check" + }, + { + "fieldname": "show_remarks", + "label": __("Show Remarks"), + "fieldtype": "Check" } + ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index d670a356975..754f17c118e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -163,6 +163,9 @@ def get_gl_entries(filters, accounting_dimensions): select_fields = """, debit, credit, debit_in_account_currency, credit_in_account_currency """ + if filters.get("show_remarks"): + select_fields += """,remarks""" + order_by_statement = "order by posting_date, account, creation" if filters.get("include_dimensions"): @@ -189,7 +192,7 @@ def get_gl_entries(filters, accounting_dimensions): voucher_type, voucher_no, {dimension_fields} cost_center, project, against_voucher_type, against_voucher, account_currency, - remarks, against, is_opening, creation {select_fields} + against, is_opening, creation {select_fields} from `tabGL Entry` where company=%(company)s {conditions} {order_by_statement} @@ -249,9 +252,7 @@ def get_conditions(filters): if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr( filters.get("company_fb") ): - frappe.throw( - _("To use a different finance book, please uncheck 'Include Default Book Entries'") - ) + frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) else: conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") else: @@ -593,8 +594,10 @@ def get_columns(filters): "width": 100, }, {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, - {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, ] ) + if filters.get("show_remarks"): + columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}]) + return columns diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 27b29baa40a..c4054a977a8 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -33,13 +33,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Accounting Dimension"), "fieldtype": "Link", "options": "Accounting Dimension", - "get_query": () =>{ - return { - filters: { - "disabled": 0 - } - } - } }, { "fieldname": "fiscal_year", diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index b29a386ae33..f6c7bd3db70 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -47,6 +47,7 @@ def get_result( out = [] for name, details in gle_map.items(): tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + bill_no, bill_date = "", "" tax_withholding_category = tax_category_map.get(name) rate = tax_rate_map.get(tax_withholding_category) @@ -68,9 +69,12 @@ def get_result( tax_amount += entry.credit - entry.debit if net_total_map.get(name): - if voucher_type == "Journal Entry": + if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount - total_amount = grand_total = base_total = tax_amount / (rate / 100) + if rate: + total_amount = grand_total = base_total = tax_amount / (rate / 100) + elif voucher_type == "Purchase Invoice": + total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name) else: total_amount, grand_total, base_total = net_total_map.get(name) else: @@ -96,7 +100,7 @@ def get_result( row.update( { - "section_code": tax_withholding_category, + "section_code": tax_withholding_category or "", "entity_type": party_map.get(party, {}).get(party_type), "rate": rate, "total_amount": total_amount, @@ -106,10 +110,14 @@ def get_result( "transaction_date": posting_date, "transaction_type": voucher_type, "ref_no": name, + "supplier_invoice_no": bill_no, + "supplier_invoice_date": bill_date, } ) out.append(row) + out.sort(key=lambda x: x["section_code"]) + return out @@ -157,14 +165,14 @@ def get_gle_map(documents): def get_columns(filters): pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ - {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, { - "label": _(filters.get("party_type")), - "fieldname": "party", - "fieldtype": "Dynamic Link", - "options": "party_type", - "width": 180, + "label": _("Section Code"), + "options": "Tax Withholding Category", + "fieldname": "section_code", + "fieldtype": "Link", + "width": 90, }, + {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, ] if filters.naming_series == "Naming Series": @@ -189,51 +197,60 @@ def get_columns(filters): columns.extend( [ - { - "label": _("Date of Transaction"), - "fieldname": "transaction_date", - "fieldtype": "Date", - "width": 100, - }, - { - "label": _("Section Code"), - "options": "Tax Withholding Category", - "fieldname": "section_code", - "fieldtype": "Link", - "width": 90, - }, {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100}, - { - "label": _("Total Amount"), - "fieldname": "total_amount", - "fieldtype": "Float", - "width": 90, - }, + ] + ) + if filters.party_type == "Supplier": + columns.extend( + [ + { + "label": _("Supplier Invoice No"), + "fieldname": "supplier_invoice_no", + "fieldtype": "Data", + "width": 120, + }, + { + "label": _("Supplier Invoice Date"), + "fieldname": "supplier_invoice_date", + "fieldtype": "Date", + "width": 120, + }, + ] + ) + + columns.extend( + [ { "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), "fieldname": "rate", "fieldtype": "Percent", - "width": 90, + "width": 60, }, { - "label": _("Tax Amount"), - "fieldname": "tax_amount", + "label": _("Total Amount"), + "fieldname": "total_amount", "fieldtype": "Float", - "width": 90, - }, - { - "label": _("Grand Total"), - "fieldname": "grand_total", - "fieldtype": "Float", - "width": 90, + "width": 120, }, { "label": _("Base Total"), "fieldname": "base_total", "fieldtype": "Float", - "width": 90, + "width": 120, }, - {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100}, + { + "label": _("Tax Amount"), + "fieldname": "tax_amount", + "fieldtype": "Float", + "width": 120, + }, + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Float", + "width": 120, + }, + {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130}, { "label": _("Reference No."), "fieldname": "ref_no", @@ -241,6 +258,12 @@ def get_columns(filters): "options": "transaction_type", "width": 180, }, + { + "label": _("Date of Transaction"), + "fieldname": "transaction_date", + "fieldtype": "Date", + "width": 100, + }, ] ) @@ -263,27 +286,7 @@ def get_tds_docs(filters): "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" ) - query_filters = { - "account": ("in", tds_accounts), - "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), - "is_cancelled": 0, - "against": ("not in", bank_accounts), - } - - party = frappe.get_all(filters.get("party_type"), pluck="name") - or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"}) - - if filters.get("party"): - del query_filters["account"] - del query_filters["against"] - or_filters = {"against": filters.get("party"), "party": filters.get("party")} - - tds_docs = frappe.get_all( - "GL Entry", - filters=query_filters, - or_filters=or_filters, - fields=["voucher_no", "voucher_type", "against", "party"], - ) + tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) for d in tds_docs: if d.voucher_type == "Purchase Invoice": @@ -319,6 +322,47 @@ def get_tds_docs(filters): ) +def get_tds_docs_query(filters, bank_accounts, tds_accounts): + if not tds_accounts: + frappe.throw( + _("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), + title=_("Accounts Missing Error"), + ) + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select("voucher_no", "voucher_type", "against", "party") + .where((gle.is_cancelled == 0)) + ) + + if filters.get("from_date"): + query = query.where(gle.posting_date >= filters.get("from_date")) + if filters.get("to_date"): + query = query.where(gle.posting_date <= filters.get("to_date")) + + if bank_accounts: + query = query.where(gle.against.notin(bank_accounts)) + + if filters.get("party"): + party = [filters.get("party")] + query = query.where( + ((gle.account.isin(tds_accounts) & gle.against.isin(party))) + | ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))) + | gle.party.isin(party) + ) + else: + party = frappe.get_all(filters.get("party_type"), pluck="name") + query = query.where( + ((gle.account.isin(tds_accounts) & gle.against.isin(party))) + | ( + (gle.voucher_type == "Journal Entry") + & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) + ) + | gle.party.isin(party) + ) + return query + + def get_journal_entry_party_map(journal_entries): journal_entry_party_map = {} for d in frappe.db.get_all( @@ -345,6 +389,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "base_tax_withholding_net_total", "grand_total", "base_total", + "bill_no", + "bill_date", ], "Sales Invoice": ["base_net_total", "grand_total", "base_total"], "Payment Entry": [ @@ -363,7 +409,13 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total] + value = [ + entry.base_tax_withholding_net_total, + entry.grand_total, + entry.base_total, + entry.bill_no, + entry.bill_date, + ] elif doctype == "Sales Invoice": value = [entry.base_net_total, entry.grand_total, entry.base_total] elif doctype == "Payment Entry": diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 6e233c802f0..ffb2931e902 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -96,7 +96,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }, { "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), + "label": __("Include Default FB Entries"), "fieldtype": "Check", "default": 1 }, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 2a8aa0c202f..8b7f0bbc006 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -275,9 +275,7 @@ def get_opening_balance( company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") 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 Entries'") - ) + frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'")) opening_balance = opening_balance.where( (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 76339713a22..01724ff55ca 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -511,7 +511,7 @@ def check_if_advance_entry_modified(args): party_account_field = ( "paid_from" if erpnext.get_party_account_type(args.party_type) == "Receivable" else "paid_to" ) - + precision = frappe.get_precision("Payment Entry", "unallocated_amount") if args.voucher_detail_no: ret = frappe.db.sql( """select t1.name @@ -533,9 +533,9 @@ def check_if_advance_entry_modified(args): where name = %(voucher_no)s and docstatus = 1 and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s - and unallocated_amount = %(unreconciled_amount)s + and round(unallocated_amount, {1}) = %(unreconciled_amount)s """.format( - party_account_field + party_account_field, precision ), args, ) @@ -624,7 +624,7 @@ def update_reference_in_payment_entry( "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), - "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation + "exchange_gain_loss": d.exchange_gain_loss, } if d.voucher_detail_no: @@ -636,28 +636,29 @@ def update_reference_in_payment_entry( existing_row.reference_doctype, existing_row.reference_name ).set_total_advance_paid() - original_row = existing_row.as_dict().copy() - existing_row.update(reference_details) + if d.allocated_amount <= existing_row.allocated_amount: + existing_row.allocated_amount -= d.allocated_amount - if d.allocated_amount < original_row.allocated_amount: new_row = payment_entry.append("references") new_row.docstatus = 1 for field in list(reference_details): - new_row.set(field, original_row[field]) + new_row.set(field, reference_details[field]) - new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount else: new_row = payment_entry.append("references") new_row.docstatus = 1 new_row.update(reference_details) payment_entry.flags.ignore_validate_update_after_submit = True + payment_entry.clear_unallocated_reference_document_rows() payment_entry.setup_party_account_field() payment_entry.set_missing_values() if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() - payment_entry.make_exchange_gain_loss_journal() + payment_entry.make_exchange_gain_loss_journal( + frappe._dict({"difference_posting_date": d.difference_posting_date}) + ) if not do_not_save: payment_entry.save(ignore_permissions=True) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5d7794e1bcc..951e612bea0 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', { frm.set_query("item_code", function() { return { "filters": { - "disabled": 0, "is_fixed_asset": 1, "is_stock_item": 0 } diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 1da3edcc60e..b092d193cc4 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -221,11 +221,11 @@ "read_only": 1 }, { + "depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)", "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "label": "Gross Purchase Amount", "options": "Company:company:default_currency", - "read_only": 1, "read_only_depends_on": "eval:!doc.is_existing_asset", "reqd": 1 }, @@ -413,6 +413,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_receipt", "fieldtype": "Link", "label": "Purchase Receipt", @@ -430,6 +431,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -555,9 +557,14 @@ "link_doctype": "Journal Entry", "link_fieldname": "reference_name", "table_fieldname": "accounts" + }, + { + "group": "Asset Capitalization", + "link_doctype": "Asset Capitalization", + "link_fieldname": "target_asset" } ], - "modified": "2023-10-03 23:28:26.732269", + "modified": "2023-11-15 17:40:17.315203", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d54d15afaf6..902a28b286c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1216,7 +1216,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "depreciation_method": d.depreciation_method, "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, - "daily_depreciation": d.daily_depreciation, + "daily_prorata_based": d.daily_prorata_based, "salvage_value_percentage": d.salvage_value_percentage, "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), @@ -1396,7 +1396,7 @@ def get_straight_line_or_manual_depr_amount( ) # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: - if row.daily_depreciation: + if row.daily_prorata_based: daily_depr_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( @@ -1441,7 +1441,7 @@ def get_straight_line_or_manual_depr_amount( ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: - if row.daily_depreciation: + if row.daily_prorata_based: daily_depr_amount = ( flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index f5fd5d60221..8e5658e318e 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -740,6 +740,15 @@ def get_disposal_account_and_cost_center(company): def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): asset_doc = frappe.get_doc("Asset", asset) + if asset_doc.available_for_use_date > getdate(disposal_date): + frappe.throw( + "Disposal date {0} cannot be before available for use date {1} of the asset.".format( + disposal_date, asset_doc.available_for_use_date + ) + ) + elif asset_doc.available_for_use_date == getdate(disposal_date): + return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation) + if asset_doc.calculate_depreciation: asset_doc.prepare_depreciation_data(getdate(disposal_date)) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fc36df8aec5..3ab53e57ae3 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -186,6 +186,7 @@ class TestAsset(AssetSetup): def test_is_fixed_asset_set(self): asset = create_asset(is_existing_asset=1) doc = frappe.new_doc("Purchase Invoice") + doc.company = "_Test Company" doc.supplier = "_Test Supplier" doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name}) @@ -487,7 +488,7 @@ class TestAsset(AssetSetup): self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account) - # CWIP: Capital Work In Progress + # Capital Work In Progress def test_cwip_accounting(self): pr = make_purchase_receipt( item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location" @@ -520,7 +521,8 @@ class TestAsset(AssetSetup): pr.submit() expected_gle = ( - ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("_Test Account Shipping Charges - _TC", 0.0, 250.0), + ("Asset Received But Not Billed - _TC", 0.0, 5000.0), ("CWIP Account - _TC", 5250.0, 0.0), ) @@ -539,9 +541,8 @@ class TestAsset(AssetSetup): expected_gle = ( ("_Test Account Service Tax - _TC", 250.0, 0.0), ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5000.0, 0.0), ("Creditors - _TC", 0.0, 5500.0), - ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), ) pi_gle = frappe.db.sql( @@ -730,7 +731,9 @@ class TestDepreciationMethods(AssetSetup): self.assertEqual(schedules, expected_schedules) - def test_schedule_for_straight_line_method_with_daily_depreciation(self): + def test_schedule_for_straight_line_method_with_daily_prorata_based( + self, + ): asset = create_asset( calculate_depreciation=1, available_for_use_date="2023-01-01", @@ -739,7 +742,7 @@ class TestDepreciationMethods(AssetSetup): depreciation_start_date="2023-01-31", total_number_of_depreciations=12, frequency_of_depreciation=1, - daily_depreciation=1, + daily_prorata_based=1, ) expected_schedules = [ @@ -1702,7 +1705,7 @@ def create_asset(**args): "total_number_of_depreciations": args.total_number_of_depreciations or 5, "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, - "daily_depreciation": args.daily_depreciation or 0, + "daily_prorata_based": args.daily_prorata_based or 0, }, ) @@ -1731,6 +1734,7 @@ def create_asset_category(): "fixed_asset_account": "_Test Fixed Asset - _TC", "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", "depreciation_expense_account": "_Test Depreciations - _TC", + "capital_work_in_progress_account": "CWIP Account - _TC", }, ) asset_category.append( diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 04654104c7a..92cb85d1b7c 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -832,12 +832,8 @@ def get_items_tagged_to_wip_composite_asset(asset): "amount", ] - pi_items = frappe.get_all( - "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields - ) - pr_items = frappe.get_all( "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields ) - return pi_items + pr_items + return pr_items diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index ea1a8112843..7c059fde2df 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -8,7 +8,7 @@ "finance_book", "depreciation_method", "total_number_of_depreciations", - "daily_depreciation", + "daily_prorata_based", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -82,23 +82,23 @@ "fieldtype": "Percent", "label": "Rate of Depreciation" }, - { - "default": "0", - "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", - "fieldname": "daily_depreciation", - "fieldtype": "Check", - "label": "Daily Depreciation" - }, { "fieldname": "salvage_value_percentage", "fieldtype": "Percent", "label": "Salvage Value Percentage" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", + "fieldname": "daily_prorata_based", + "fieldtype": "Check", + "label": "Depreciate based on daily pro-rata" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-29 15:39:52.740594", + "modified": "2023-11-03 22:21:52.090191", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", 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 48b17f58fb2..0497addf44a 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -52,7 +52,7 @@ frappe.query_reports["Fixed Asset Register"] = { }, { "fieldname": "include_default_book_assets", - "label": __("Include Default Book Assets"), + "label": __("Include Default FB Assets"), "fieldtype": "Check", "default": 1 }, 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 383be973477..45811a93444 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -223,7 +223,7 @@ def get_assets_linked_to_fb(filters): company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") 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'")) + frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'")) query = query.where( (afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 71cb01b188f..059999245d1 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -16,7 +16,7 @@ "transaction_settings_section", "po_required", "pr_required", - "over_order_allowance", + "blanket_order_allowance", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -159,19 +159,19 @@ "fieldtype": "Check", "label": "Set Landed Cost Based on Purchase Invoice Rate" }, - { - "default": "0", - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "fieldname": "over_order_allowance", - "fieldtype": "Float", - "label": "Over Order Allowance (%)" - }, { "default": "0", "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", "fieldname": "use_transaction_date_exchange_rate", "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate" + }, + { + "default": "0", + "description": "Percentage you are allowed to order beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -179,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-16 16:22:03.201078", + "modified": "2023-10-25 14:03:32.520418", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 06b9d29e69c..69f34c33b69 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -522,6 +522,9 @@ def make_purchase_receipt(source_name, target_doc=None): "bom": "bom", "material_request": "material_request", "material_request_item": "material_request_item", + "sales_order": "sales_order", + "sales_order_item": "sales_order_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) @@ -598,6 +601,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "field_map": { "name": "po_detail", "parent": "purchase_order", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index fe1b9702539..0b390e542a9 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -86,6 +86,8 @@ "billed_amt", "accounting_details", "expense_account", + "column_break_fyqr", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -188,6 +190,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -469,6 +472,7 @@ "fieldname": "material_request", "fieldtype": "Link", "label": "Material Request", + "mandatory_depends_on": "eval: doc.material_request_item", "no_copy": 1, "oldfieldname": "prevdoc_docname", "oldfieldtype": "Link", @@ -484,6 +488,7 @@ "fieldtype": "Data", "hidden": 1, "label": "Material Request Item", + "mandatory_depends_on": "eval: doc.material_request", "no_copy": 1, "oldfieldname": "prevdoc_detail_docname", "oldfieldtype": "Data", @@ -897,13 +902,23 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" + }, + { + "fieldname": "column_break_fyqr", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-13 16:22:40.825092", + "modified": "2023-11-14 18:34:27.267382", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index e07f4626b8f..1b171c8e9eb 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -88,6 +88,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -261,13 +262,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-24 17:26:46.276934", + "modified": "2023-11-14 18:34:48.327224", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 3aca21ed6a6..5ac8cde50fe 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -170,7 +170,7 @@ "fieldname": "supplier_type", "fieldtype": "Select", "label": "Supplier Type", - "options": "Company\nIndividual", + "options": "Company\nIndividual\nProprietorship\nPartnership", "reqd": 1 }, { @@ -457,7 +457,7 @@ "link_fieldname": "party" } ], - "modified": "2023-05-09 15:34:13.408932", + "modified": "2023-10-19 16:55:15.148325", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", @@ -525,4 +525,4 @@ "states": [], "title_field": "supplier_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 11bb06e0caa..3bd306e6591 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -8,7 +8,7 @@ def get_data(): "This is based on transactions against this Supplier. See timeline below for details" ), "fieldname": "supplier", - "non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"}, + "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, "transactions": [ {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 638cde01be5..a4e3c4f32e2 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -134,6 +134,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -560,13 +561,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-19 12:36:26.913211", + "modified": "2023-11-14 18:35:03.435817", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index fd73b870c59..579c0a65ad9 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = { } } } - else { - return { - filters: { "disabled": 0 } - } - } } }, { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c17866162b6..9a34af2c66c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1131,7 +1131,9 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), ): - posting_date = frappe.db.get_value(arg.voucher_type, arg.voucher_no, "posting_date") + posting_date = arg.get("difference_posting_date") or frappe.db.get_value( + arg.voucher_type, arg.voucher_no, "posting_date" + ) je = create_gain_loss_journal( self.company, posting_date, @@ -1214,7 +1216,7 @@ class AccountsController(TransactionBase): je = create_gain_loss_journal( self.company, - self.posting_date, + args.get("difference_posting_date") if args else self.posting_date, self.party_type, self.party, party_account, @@ -2247,6 +2249,7 @@ class AccountsController(TransactionBase): repost_ledger = frappe.new_doc("Repost Accounting Ledger") repost_ledger.company = self.company repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) + repost_ledger.flags.ignore_permissions = True repost_ledger.insert() repost_ledger.submit() self.db_set("repost_required", 0) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a38905c7e2b..913e2274b1a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -4,7 +4,7 @@ import frappe from frappe import ValidationError, _, msgprint -from frappe.contacts.doctype.address.address import get_address_display +from frappe.contacts.doctype.address.address import render_address from frappe.utils import cint, cstr, flt, getdate from frappe.utils.data import nowtime @@ -78,26 +78,26 @@ class BuyingController(SubcontractingController): def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: for row in self.items: + if row.rate <= 0: + # override the rate with valuation rate + row.rate = get_incoming_rate( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.get("posting_date"), + "posting_time": self.get("posting_time"), + "qty": row.qty, + "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + }, + raise_error_if_no_rate=False, + ) - # override the rate with valuation rate - row.rate = get_incoming_rate( - { - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.get("posting_date"), - "posting_time": self.get("posting_time"), - "qty": row.qty, - "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - }, - raise_error_if_no_rate=False, - ) - - row.discount_percentage = 0.0 - row.discount_amount = 0.0 - row.margin_rate_or_amount = 0.0 + row.discount_percentage = 0.0 + row.discount_amount = 0.0 + row.margin_rate_or_amount = 0.0 def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -219,7 +219,9 @@ class BuyingController(SubcontractingController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def set_total_in_words(self): from frappe.utils import money_in_words @@ -336,7 +338,7 @@ class BuyingController(SubcontractingController): { "item_code": d.item_code, "warehouse": d.get("from_warehouse"), - "posting_date": self.get("posting_date") or self.get("transation_date"), + "posting_date": self.get("posting_date") or self.get("transaction_date"), "posting_time": posting_time, "qty": -1 * flt(d.get("stock_qty")), "serial_no": d.get("serial_no"), diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 36225e3dd5e..b73ebf53ae8 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -560,6 +560,8 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): if filters.get("company"): condition += "and tabAccount.company = %(company)s" + condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}" + return frappe.db.sql( """select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5daaba8e892..f005a7f7a39 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,9 +4,9 @@ import frappe from frappe import _, bold, throw -from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime +from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController @@ -591,7 +591,9 @@ class SellingController(StockController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def validate_for_duplicate_items(self): check_list, chk_dupl_itm = [], [] diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 73a248fb531..d09001c8fc1 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -47,15 +47,15 @@ status_map = { ], [ "To Bill", - "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1", + "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1", ], [ "To Deliver", - "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note", + "eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note", ], [ "Completed", - "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", + "eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1", ], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e24d8fb661f..403f71be5eb 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -62,9 +62,12 @@ class StockController(AccountsController): ) ) + is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items")) + if ( cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items + or is_asset_pr ): warehouse_account = get_warehouse_account_map(self.company) @@ -73,11 +76,6 @@ class StockController(AccountsController): gl_entries = self.get_gl_entries(warehouse_account) make_gl_entries(gl_entries, from_repost=from_repost) - elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1: - gl_entries = [] - gl_entries = self.get_asset_gl_entry(gl_entries) - make_gl_entries(gl_entries, from_repost=from_repost) - def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -670,13 +668,21 @@ class StockController(AccountsController): d.stock_uom_rate = d.rate / (d.conversion_factor or 1) def validate_internal_transfer(self): - if ( - self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") - and self.is_internal_transfer() - ): - self.validate_in_transit_warehouses() - self.validate_multi_currency() - self.validate_packed_items() + if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"): + if self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + else: + self.validate_internal_transfer_warehouse() + + def validate_internal_transfer_warehouse(self): + for row in self.items: + if row.get("target_warehouse"): + row.target_warehouse = None + + if row.get("from_warehouse"): + row.from_warehouse = None def validate_in_transit_warehouses(self): if ( @@ -1037,8 +1043,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa repost_entry = frappe.new_doc("Repost Item Valuation") repost_entry.based_on = "Item and Warehouse" - repost_entry.voucher_type = voucher_type - repost_entry.voucher_no = voucher_no repost_entry.item_code = sle.item_code repost_entry.warehouse = sle.warehouse diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 391258fde77..97d3c5c32de 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -7,7 +7,7 @@ import frappe from frappe import qb from frappe.query_builder.functions import Sum from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate +from frappe.utils import add_days, flt, getdate, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -614,6 +614,73 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) + def test_15_gain_loss_on_different_posting_date(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice( + posting_date=add_days(nowdate(), -2), qty=2, conversion_rate=80, rate=1 + ) + # Payment + pe = ( + self.create_payment_entry(posting_date=add_days(nowdate(), -1), amount=2, source_exc_rate=75) + .save() + .submit() + ) + + # There should be outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Reconcile the remaining amount + pr = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].gain_loss_posting_date = add_days(nowdate(), 1) + pr.reconcile() + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + self.assertEqual( + frappe.db.get_value("Journal Entry", exc_je_for_si[0].parent, "posting_date"), + getdate(add_days(nowdate(), 1)), + ) + + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Cancel Payment + pe.reload() + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/crm/doctype/opportunity_item/opportunity_item.json b/erpnext/crm/doctype/opportunity_item/opportunity_item.json index 1b4973c1b2b..732f80d01c9 100644 --- a/erpnext/crm/doctype/opportunity_item/opportunity_item.json +++ b/erpnext/crm/doctype/opportunity_item/opportunity_item.json @@ -103,6 +103,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -165,7 +166,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-07-30 16:39:09.775720", + "modified": "2023-11-14 18:35:30.887278", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity Item", @@ -173,5 +174,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e57a30a88e1..a462141a86b 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.desk.doctype.tag.tag import add_tag from frappe.model.document import Document -from frappe.utils import add_months, formatdate, getdate, today +from frappe.utils import add_months, formatdate, getdate, sbool, today from plaid.errors import ItemError from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account @@ -238,8 +238,6 @@ def new_bank_transaction(transaction): deposit = abs(amount) withdrawal = 0.0 - status = "Pending" if transaction["pending"] == "True" else "Settled" - tags = [] try: tags += transaction["category"] @@ -247,13 +245,14 @@ def new_bank_transaction(transaction): except KeyError: pass - if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): + if not frappe.db.exists( + "Bank Transaction", dict(transaction_id=transaction["transaction_id"]) + ) and not sbool(transaction["pending"]): try: new_transaction = frappe.get_doc( { "doctype": "Bank Transaction", "date": getdate(transaction["date"]), - "status": status, "bank_account": bank_account, "deposit": deposit, "withdrawal": withdrawal, diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b2a76f2038c..6d5dddd9e97 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -517,6 +517,7 @@ accounting_dimension_doctypes = [ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", + "Sales Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 0c9c97f60fd..4a333df5a59 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -411,11 +411,6 @@ def close_unsecured_term_loan(loan): frappe.throw(_("Cannot close this loan until full repayment")) -def close_loan(loan, total_amount_paid): - frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) - frappe.db.set_value("Loan", loan, "status", "Closed") - - @frappe.whitelist() def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0): disbursement_entry = frappe.new_doc("Loan Disbursement") diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 25aecf673bb..e920f08c70f 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( + get_pending_principal_amount, +) class LoanWriteOff(AccountsController): @@ -39,11 +42,13 @@ class LoanWriteOff(AccountsController): def on_submit(self): self.update_outstanding_amount() self.make_gl_entries() + self.close_employee_loan() def on_cancel(self): self.update_outstanding_amount(cancel=1) self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] self.make_gl_entries(cancel=1) + self.close_employee_loan(cancel=1) def update_outstanding_amount(self, cancel=0): written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount") @@ -94,3 +99,39 @@ class LoanWriteOff(AccountsController): ) make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) + + def close_employee_loan(self, cancel=0): + if not frappe.db.has_column("Loan", "repay_from_salary"): + return + + loan = frappe.get_value( + "Loan", + self.loan, + [ + "total_payment", + "total_principal_paid", + "loan_amount", + "total_interest_payable", + "written_off_amount", + "disbursed_amount", + "status", + "is_secured_loan", + "repay_from_salary", + "name", + ], + as_dict=1, + ) + + if loan.is_secured_loan or not loan.repay_from_salary: + return + + if not cancel: + pending_principal_amount = get_pending_principal_amount(loan) + + precision = cint(frappe.db.get_default("currency_precision")) or 2 + + if flt(pending_principal_amount, precision) <= 0: + frappe.db.set_value("Loan", loan.name, "status", "Closed") + frappe.msgprint(_("Loan {0} closed").format(loan.name)) + else: + frappe.db.set_value("Loan", loan.loan, "status", "Disbursed") diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 32f1c365ade..0135a4f9712 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -107,7 +107,7 @@ def validate_against_blanket_order(order_doc): allowance = flt( frappe.db.get_single_value( "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", - "over_order_allowance", + "blanket_order_allowance", ) ) for bo_name, item_data in order_data.items(): diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 58f3c950598..e9fc25b5bcb 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,7 +63,7 @@ class TestBlanketOrder(FrappeTestCase): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) - def test_over_order_allowance(self): + def test_blanket_order_allowance(self): # Sales Order bo = make_blanket_order(blanket_order_type="Selling", quantity=100) @@ -74,7 +74,7 @@ class TestBlanketOrder(FrappeTestCase): so.items[0].qty = 110 self.assertRaises(frappe.ValidationError, so.submit) - frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 10) so.submit() # Purchase Order @@ -87,7 +87,7 @@ class TestBlanketOrder(FrappeTestCase): po.items[0].qty = 110 self.assertRaises(frappe.ValidationError, po.submit) - frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8058a5f8b75..8d593d12963 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1172,12 +1172,12 @@ def get_children(parent=None, is_root=False, **filters): def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] - expenses_included_in_valuation = frappe.get_cached_value( - "Company", work_order.company, "expenses_included_in_valuation" + default_expense_account = frappe.get_cached_value( + "Company", work_order.company, "default_expense_account" ) - add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation) - add_operations_cost(stock_entry, work_order, expenses_included_in_valuation) + add_non_stock_items_cost(stock_entry, work_order, default_expense_account) + add_operations_cost(stock_entry, work_order, default_expense_account) def add_non_stock_items_cost(stock_entry, work_order, expense_account): diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index 9b1db63494b..c75ac32cd12 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -85,6 +85,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -169,7 +170,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-05-27 13:42:23.305455", + "modified": "2023-11-14 18:35:40.856895", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index c5266119dc2..cb58af1f29a 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -111,6 +111,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -289,7 +290,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-07-28 10:20:51.559010", + "modified": "2023-11-14 18:35:51.378513", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index f4877fdca0b..9e320853514 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = { }, gantt: { field_map: { - "start": "started_time", - "end": "started_time", + "start": "expected_start_date", + "end": "expected_end_date", "id": "name", "title": "subject", "color": "color", diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 5d883bf9fa7..99fca9570f7 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,6 +1,6 @@ frappe.listview_settings['Job Card'] = { has_indicator_for_draft: true, - + add_fields: ["expected_start_date", "expected_end_date"], get_indicator: function(doc) { const status_colors = { "Work In Progress": "orange", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 72438ddceea..dd102b0fae0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -89,10 +89,6 @@ frappe.ui.form.on('Production Plan', { frm.trigger("show_progress"); if (frm.doc.status !== "Completed") { - frm.add_custom_button(__("Work Order Tree"), ()=> { - frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name}); - }, __('View')); - frm.add_custom_button(__("Production Plan Summary"), ()=> { frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name}); }, __('View')); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 4a0041662bc..49386c4ebc4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -36,6 +36,7 @@ "prod_plan_references", "section_break_24", "combine_sub_items", + "sub_assembly_warehouse", "section_break_ucc4", "skip_available_sub_assembly_item", "column_break_igxl", @@ -416,13 +417,19 @@ { "fieldname": "column_break_igxl", "fieldtype": "Column Break" + }, + { + "fieldname": "sub_assembly_warehouse", + "fieldtype": "Link", + "label": "Sub Assembly Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-09-29 11:41:03.246059", + "modified": "2023-11-03 14:08:11.928027", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5fc764fb6f7..8d12ba92103 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -490,6 +490,12 @@ class ProductionPlan(Document): bin = frappe.get_doc("Bin", bin_name, for_update=True) bin.update_reserved_qty_for_production_plan() + for d in self.sub_assembly_items: + if d.fg_warehouse and d.type_of_manufacturing == "In House": + bin_name = get_or_make_bin(d.production_item, d.fg_warehouse) + bin = frappe.get_doc("Bin", bin_name, for_update=True) + bin.update_reserved_qty_for_for_sub_assembly() + def delete_draft_work_order(self): for d in frappe.get_all( "Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)} @@ -808,7 +814,11 @@ class ProductionPlan(Document): bom_data = [] - warehouse = row.warehouse if self.skip_available_sub_assembly_item else None + warehouse = ( + (self.sub_assembly_warehouse or row.warehouse) + if self.skip_available_sub_assembly_item + else None + ) get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) @@ -817,8 +827,6 @@ class ProductionPlan(Document): # Combine subassembly items sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store) - sub_assembly_items_store.sort(key=lambda d: d.bom_level, reverse=True) # sort by bom level - for idx, row in enumerate(sub_assembly_items_store): row.idx = idx + 1 self.append("sub_assembly_items", row) @@ -830,7 +838,7 @@ class ProductionPlan(Document): for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name - data.fg_warehouse = row.warehouse + data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse data.schedule_date = row.planned_start_date data.type_of_manufacturing = manufacturing_type or ( "Subcontract" if data.is_sub_contracted_item else "In House" @@ -1636,8 +1644,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): query = query.run() - if not query: - return 0.0 + if not query or query[0][0] is None: + return None reserved_qty_for_production_plan = flt(query[0][0]) @@ -1734,7 +1742,10 @@ def get_raw_materials_of_sub_assembly_items( if not item.conversion_factor and item.purchase_uom: item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) - item_details.setdefault(item.get("item_code"), item) + if details := item_details.get(item.get("item_code")): + details.qty += item.get("qty") + else: + item_details.setdefault(item.get("item_code"), item) return item_details @@ -1776,3 +1787,29 @@ def sales_order_query( query = query.offset(start) return query.run() + + +def get_reserved_qty_for_sub_assembly(item_code, warehouse): + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Production Plan Sub Assembly Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(Sum(child.qty - IfNull(child.wo_produced_qty, 0))) + .where( + (table.docstatus == 1) + & (child.production_item == item_code) + & (child.fg_warehouse == warehouse) + & (table.status.notin(["Completed", "Closed"])) + ) + ) + + query = query.run() + + if not query or query[0][0] is None: + return None + + qty = flt(query[0][0]) + return qty if qty > 0 else 0.0 diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index dbd3083ab58..4ad7d06c707 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -653,49 +653,6 @@ class TestProductionPlan(FrappeTestCase): frappe.db.rollback() - def test_subassmebly_sorting(self): - "Test subassembly sorting in case of multiple items with nested BOMs." - from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom - - prefix = "_TestLevel_" - boms = { - "Assembly": { - "SubAssembly1": { - "ChildPart1": {}, - "ChildPart2": {}, - }, - "ChildPart6": {}, - "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, - }, - "MegaDeepAssy": { - "SecretSubassy": { - "SecretPart": {"VerySecret": {"SuperSecret": {"Classified": {}}}}, - }, - # ^ assert that this is - # first item in subassy table - }, - } - create_nested_bom(boms, prefix=prefix) - - items = [prefix + item_code for item_code in boms.keys()] - plan = create_production_plan(item_code=items[0], do_not_save=True) - plan.append( - "po_items", - { - "use_multi_level_bom": 1, - "item_code": items[1], - "bom_no": frappe.db.get_value("Item", items[1], "default_bom"), - "planned_qty": 1, - "planned_start_date": now_datetime(), - }, - ) - plan.get_sub_assembly_items() - - bom_level_order = [d.bom_level for d in plan.sub_assembly_items] - self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True)) - # lowest most level of subassembly should be first - self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) - def test_multiple_work_order_for_production_plan_item(self): "Test producing Prod Plan (making WO) in parts." @@ -1031,13 +988,14 @@ class TestProductionPlan(FrappeTestCase): after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertEqual(after_qty - before_qty, 1) - pln = frappe.get_doc("Production Plan", pln.name) pln.cancel() bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + pln.reload() + self.assertEqual(pln.docstatus, 2) self.assertEqual(after_qty, before_qty) def test_resered_qty_for_production_plan_for_work_order(self): @@ -1321,6 +1279,120 @@ class TestProductionPlan(FrappeTestCase): self.assertTrue(row.warehouse == mrp_warhouse) self.assertEqual(row.quantity, 12) + def test_mr_qty_for_same_rm_with_different_sub_assemblies(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "Fininshed Goods2 For SUB Test": { + "SubAssembly2 For SUB Test": {"ChildPart2 For SUB Test": {}}, + "SubAssembly3 For SUB Test": {"ChildPart2 For SUB Test": {}}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + plan.get_sub_assembly_items() + plan.make_material_request() + + for row in plan.mr_items: + if row.item_code == "ChildPart2 For SUB Test": + self.assertEqual(row.quantity, 2) + + def test_reserve_sub_assembly_items(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + bom_tree = { + "Fininshed Goods Bicycle": { + "Frame Assembly": {"Frame": {}}, + "Chain Assembly": {"Chain": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + company = "_Test Company" + + sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company) + + for item_code in ["Frame", "Chain"]: + make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100) + + before_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=2, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse=warehouse, + sub_assembly_warehouse=sub_assembly_warehouse, + ) + + plan.get_sub_assembly_items() + plan.submit() + + after_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + self.assertEqual(after_qty, before_qty + 2) + + plan.make_work_order() + work_orders = frappe.get_all( + "Work Order", + fields=["name", "production_item"], + filters={"production_plan": plan.name}, + order_by="creation desc", + ) + + for d in work_orders: + wo_doc = frappe.get_doc("Work Order", d.name) + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + + wo_doc.wip_warehouse = ( + warehouse + if d.production_item in ["Frame Assembly", "Chain Assembly"] + else sub_assembly_warehouse + ) + + wo_doc.submit() + + if d.production_item == "Frame Assembly": + self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse) + se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2)) + se_doc.submit() + + after_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse}, + "reserved_qty_for_production_plan", + ) + ) + + self.assertEqual(after_qty, before_qty) + def create_production_plan(**args): """ @@ -1341,6 +1413,7 @@ def create_production_plan(**args): "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, "get_items_from": "Sales Order", "skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0, + "sub_assembly_warehouse": args.sub_assembly_warehouse, } ) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index fde0404c019..aff740b732e 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -17,11 +17,10 @@ "type_of_manufacturing", "supplier", "work_order_details_section", - "work_order", + "wo_produced_qty", "purchase_order", "production_plan_item", "column_break_7", - "produced_qty", "received_qty", "indent", "section_break_19", @@ -52,13 +51,6 @@ "fieldtype": "Section Break", "label": "Reference" }, - { - "fieldname": "work_order", - "fieldtype": "Link", - "label": "Work Order", - "options": "Work Order", - "read_only": 1 - }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -81,7 +73,8 @@ { "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Qty" + "label": "Received Qty", + "read_only": 1 }, { "fieldname": "bom_no", @@ -161,12 +154,6 @@ "label": "Target Warehouse", "options": "Warehouse" }, - { - "fieldname": "produced_qty", - "fieldtype": "Data", - "label": "Produced Quantity", - "read_only": 1 - }, { "default": "In House", "fieldname": "type_of_manufacturing", @@ -209,12 +196,18 @@ "label": "Projected Qty", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "wo_produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-22 17:52:34.708879", + "modified": "2023-11-03 13:33:42.959387", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index c1a078d65e0..88747b8f7a6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -710,7 +710,7 @@ erpnext.work_order = { return new Promise((resolve, reject) => { frappe.prompt({ fieldtype: 'Float', - label: __('Qty for {0}', [purpose]), + label: __('Qty for {0}', [__(purpose)]), fieldname: 'qty', description: __('Max: {0}', [max]), default: max diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 93d015dc93b..f95b4f66a33 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -297,6 +297,7 @@ class WorkOrder(Document): update_produced_qty_in_so_item(self.sales_order, self.sales_order_item) if self.production_plan: + self.set_produced_qty_for_sub_assembly_item() self.update_production_plan_status() def get_transferred_or_manufactured_qty(self, purpose): @@ -544,16 +545,49 @@ class WorkOrder(Document): ) def update_planned_qty(self): + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_sub_assembly, + ) + + qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)} + + if self.production_plan_sub_assembly_item and self.production_plan: + qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly( + self.production_item, self.fg_warehouse + ) + update_bin_qty( self.production_item, self.fg_warehouse, - {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}, + qty_dict, ) if self.material_request: mr_obj = frappe.get_doc("Material Request", self.material_request) mr_obj.update_requested_qty([self.material_request_item]) + def set_produced_qty_for_sub_assembly_item(self): + table = frappe.qb.DocType("Work Order") + + query = ( + frappe.qb.from_(table) + .select(Sum(table.produced_qty)) + .where( + (table.production_plan == self.production_plan) + & (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item) + & (table.docstatus == 1) + ) + ).run() + + produced_qty = flt(query[0][0]) if query else 0 + + frappe.db.set_value( + "Production Plan Sub Assembly Item", + self.production_plan_sub_assembly_item, + "wo_produced_qty", + produced_qty, + ) + def update_ordered_qty(self): if ( self.production_plan diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 0eb22a22f73..30974f170c4 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = { "options": "Item", "get_query": () =>{ return { - filters: { "disabled": 0, "is_stock_item": 1 } + filters: { "is_stock_item": 1 } } } }, diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js index 59396fef16e..a4d7fae4560 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js @@ -22,9 +22,9 @@ frappe.query_reports["Production Plan Summary"] = { "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname == "document_name") { + if (column.fieldname == "item_code") { var color = data.pending_qty > 0 ? 'red': 'green'; - value = `${data['document_name']}`; + value = `${data['item_code']}`; } return value; diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 2c8f82f2cc6..076690ff090 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -44,6 +44,7 @@ def get_production_plan_item_details(filters, data, order_details): { "indent": 0, "item_code": row.item_code, + "sales_order": row.get("sales_order"), "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), "qty": row.planned_qty, "document_type": "Work Order", @@ -80,7 +81,7 @@ def get_production_plan_sub_assembly_item_details( data.append( { - "indent": 1, + "indent": 1 + item.indent, "item_code": item.production_item, "item_name": item.item_name, "qty": item.qty, @@ -98,7 +99,7 @@ def get_work_order_details(filters, order_details): for row in frappe.get_all( "Work Order", filters={"production_plan": filters.get("production_plan")}, - fields=["name", "produced_qty", "production_plan", "production_item"], + fields=["name", "produced_qty", "production_plan", "production_item", "sales_order"], ): order_details.setdefault((row.name, row.production_item), row) @@ -118,10 +119,17 @@ def get_column(filters): "label": _("Finished Good"), "fieldtype": "Link", "fieldname": "item_code", - "width": 300, + "width": 240, "options": "Item", }, - {"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 100}, + {"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 150}, + { + "label": _("Sales Order"), + "options": "Sales Order", + "fieldtype": "Link", + "fieldname": "sales_order", + "width": 100, + }, { "label": _("Document Type"), "fieldtype": "Link", @@ -133,10 +141,16 @@ def get_column(filters): "label": _("Document Name"), "fieldtype": "Dynamic Link", "fieldname": "document_name", - "width": 150, + "options": "document_type", + "width": 180, }, {"label": _("BOM Level"), "fieldtype": "Int", "fieldname": "bom_level", "width": 100}, {"label": _("Order Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 120}, - {"label": _("Received Qty"), "fieldtype": "Float", "fieldname": "produced_qty", "width": 160}, + { + "label": _("Produced / Received Qty"), + "fieldtype": "Float", + "fieldname": "produced_qty", + "width": 200, + }, {"label": _("Pending Qty"), "fieldtype": "Float", "fieldname": "pending_qty", "width": 110}, ] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 37e70967388..86984819816 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,5 +341,13 @@ execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults +erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item +erpnext.patches.v14_0.rename_over_order_allowance_field +erpnext.patches.v14_0.migrate_delivery_stop_lock_field +execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) +execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) +erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month +erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based +erpnext.patches.v14_0.add_default_for_repost_settings # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py deleted file mode 100644 index 9588e026d34..00000000000 --- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py +++ /dev/null @@ -1,42 +0,0 @@ -import frappe - - -def execute(): - frappe.reload_doctype("Landed Cost Taxes and Charges") - - company_account_map = frappe._dict( - frappe.db.sql( - """ - SELECT name, expenses_included_in_valuation from `tabCompany` - """ - ) - ) - - for company, account in company_account_map.items(): - frappe.db.sql( - """ - UPDATE - `tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l - SET - t.expense_account = %s - WHERE - l.docstatus = 1 - AND l.company = %s - AND t.parent = l.name - """, - (account, company), - ) - - frappe.db.sql( - """ - UPDATE - `tabLanded Cost Taxes and Charges` t, `tabStock Entry` s - SET - t.expense_account = %s - WHERE - s.docstatus = 1 - AND s.company = %s - AND t.parent = s.name - """, - (account, company), - ) diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index efbb96c100a..e53bdf8f19e 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -3,23 +3,24 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "quality_inspection_parameter") + params = set() - # get all distinct parameters from QI readigs table - reading_params = frappe.db.get_all( - "Quality Inspection Reading", fields=["distinct specification"] - ) - reading_params = [d.specification for d in reading_params] + # get all parameters from QI readings table + for (p,) in frappe.db.get_all( + "Quality Inspection Reading", fields=["specification"], as_list=True + ): + params.add(p.strip()) - # get all distinct parameters from QI Template as some may be unused in QI - template_params = frappe.db.get_all( - "Item Quality Inspection Parameter", fields=["distinct specification"] - ) - template_params = [d.specification for d in template_params] + # get all parameters from QI Template as some may be unused in QI + for (p,) in frappe.db.get_all( + "Item Quality Inspection Parameter", fields=["specification"], as_list=True + ): + params.add(p.strip()) - params = list(set(reading_params + template_params)) + # because db primary keys are case insensitive, so duplicates will cause an exception + params = set({x.casefold(): x for x in params}.values()) for parameter in params: - if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc( - {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} - ).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 0bd3fcdec4c..30db996e736 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -125,6 +125,7 @@ def execute(): loan_type_doc.company = loan.company loan_type_doc.mode_of_payment = loan.mode_of_payment loan_type_doc.payment_account = loan.payment_account + loan_type_doc.disbursement_account = loan.payment_account loan_type_doc.loan_account = loan.loan_account loan_type_doc.interest_income_account = loan.interest_income_account loan_type_doc.penalty_income_account = penalty_account diff --git a/erpnext/patches/v14_0/add_default_for_repost_settings.py b/erpnext/patches/v14_0/add_default_for_repost_settings.py new file mode 100644 index 00000000000..6cafc66aab8 --- /dev/null +++ b/erpnext/patches/v14_0/add_default_for_repost_settings.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + """ + Update Repost Accounting Ledger Settings with default values + """ + allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") + for x in allowed_types: + repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) + repost_settings.save() diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py new file mode 100644 index 00000000000..8f77c35b129 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Sales Order Item") diff --git a/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py new file mode 100644 index 00000000000..c9ec1e113dc --- /dev/null +++ b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py @@ -0,0 +1,7 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Delivery Stop", "lock"): + rename_field("Delivery Stop", "lock", "locked") diff --git a/erpnext/patches/v14_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py b/erpnext/patches/v14_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py new file mode 100644 index 00000000000..9666c1734bb --- /dev/null +++ b/erpnext/patches/v14_0/rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.utils.rename_field import rename_field + + +def execute(): + try: + rename_field( + "Asset Finance Book", "daily_depreciation", "depreciation_amount_based_on_num_days_in_month" + ) + + except Exception as e: + if e.args[0] != 1054: + raise diff --git a/erpnext/patches/v14_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py b/erpnext/patches/v14_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py new file mode 100644 index 00000000000..96a16ebf2b5 --- /dev/null +++ b/erpnext/patches/v14_0/rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.utils.rename_field import rename_field + + +def execute(): + try: + rename_field( + "Asset Finance Book", "depreciation_amount_based_on_num_days_in_month", "daily_prorata_based" + ) + + except Exception as e: + if e.args[0] != 1054: + raise diff --git a/erpnext/patches/v14_0/rename_over_order_allowance_field.py b/erpnext/patches/v14_0/rename_over_order_allowance_field.py new file mode 100644 index 00000000000..a81fe888c2a --- /dev/null +++ b/erpnext/patches/v14_0/rename_over_order_allowance_field.py @@ -0,0 +1,15 @@ +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field( + "Buying Settings", + "over_order_allowance", + "blanket_order_allowance", + ) + + rename_field( + "Selling Settings", + "over_order_allowance", + "blanket_order_allowance", + ) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 47b88a002bc..72845b20742 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -28,7 +28,6 @@ frappe.ui.form.on(cur_frm.doctype, { filters: { "account_type": account_type, "company": doc.company, - "disabled": 0 } } }); diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index bbdd51d6e54..fa00ed23620 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -17,7 +17,7 @@ erpnext.accounts.unreconcile_payments = { }, callback: function(r) { if (r.message) { - frm.add_custom_button(__("Un-Reconcile"), function() { + frm.add_custom_button(__("UnReconcile"), function() { erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); }, __('Actions')); } @@ -87,11 +87,11 @@ erpnext.accounts.unreconcile_payments = { unreconcile_dialog_fields[0].get_data = function(){ return r.message}; let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', + title: 'UnReconcile Allocations', fields: unreconcile_dialog_fields, size: 'large', cannot_add_rows: true, - primary_action_label: 'Un-Reconcile', + primary_action_label: 'UnReconcile', primary_action(values) { let selected_allocations = values.allocations.filter(x=>x.__checked); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index fd2b6a4eaa0..79fd2ebdbe9 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -3,10 +3,10 @@ frappe.ui.form.on('Quality Procedure', { refresh: function(frm) { - frm.set_query("procedure","processes", (frm) =>{ + frm.set_query('procedure', 'processes', (frm) =>{ return { filters: { - name: ["not in", [frm.parent_quality_procedure, frm.name]] + name: ['not in', [frm.parent_quality_procedure, frm.name]] } }; }); @@ -14,7 +14,8 @@ frappe.ui.form.on('Quality Procedure', { frm.set_query('parent_quality_procedure', function(){ return { filters: { - is_group: 1 + is_group: 1, + name: ['!=', frm.doc.name] } }; }); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index e8604080fbf..6834abc9d41 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -16,16 +16,13 @@ class QualityProcedure(NestedSet): def on_update(self): NestedSet.on_update(self) self.set_parent() + self.remove_parent_from_old_child() + self.add_child_to_parent() + self.remove_child_from_old_parent() def after_insert(self): self.set_parent() - - # add child to parent if missing - if self.parent_quality_procedure: - parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) - if not [d for d in parent.processes if d.procedure == self.name]: - parent.append("processes", {"procedure": self.name, "process_description": self.name}) - parent.save() + self.add_child_to_parent() def on_trash(self): # clear from child table (sub procedures) @@ -36,15 +33,6 @@ class QualityProcedure(NestedSet): ) NestedSet.on_trash(self, allow_root_deletion=True) - def set_parent(self): - for process in self.processes: - # Set parent for only those children who don't have a parent - has_parent = frappe.db.get_value( - "Quality Procedure", process.procedure, "parent_quality_procedure" - ) - if not has_parent and process.procedure: - frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) - def check_for_incorrect_child(self): for process in self.processes: if process.procedure: @@ -61,6 +49,48 @@ class QualityProcedure(NestedSet): title=_("Invalid Child Procedure"), ) + def set_parent(self): + """Set `Parent Procedure` in `Child Procedures`""" + + for process in self.processes: + if process.procedure: + if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"): + frappe.db.set_value( + "Quality Procedure", process.procedure, "parent_quality_procedure", self.name + ) + + def remove_parent_from_old_child(self): + """Remove `Parent Procedure` from `Old Child Procedures`""" + + if old_doc := self.get_doc_before_save(): + if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]): + current_child_procedures = set([d.procedure for d in self.processes if d.procedure]) + + if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)): + for child_procedure in removed_child_procedures: + frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None) + + def add_child_to_parent(self): + """Add `Child Procedure` to `Parent Procedure`""" + + if self.parent_quality_procedure: + parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) + if not [d for d in parent.processes if d.procedure == self.name]: + parent.append("processes", {"procedure": self.name, "process_description": self.name}) + parent.save() + + def remove_child_from_old_parent(self): + """Remove `Child Procedure` from `Old Parent Procedure`""" + + if old_doc := self.get_doc_before_save(): + if old_parent := old_doc.parent_quality_procedure: + if self.parent_quality_procedure != old_parent: + parent = frappe.get_doc("Quality Procedure", old_parent) + for process in parent.processes: + if process.procedure == self.name: + parent.remove(process) + parent.save() + @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index 04e82112142..467186debd9 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -1,56 +1,107 @@ # Copyright (c) 2018, Frappe and Contributors # See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from .quality_procedure import add_node -class TestQualityProcedure(unittest.TestCase): +class TestQualityProcedure(FrappeTestCase): def test_add_node(self): - try: - procedure = frappe.get_doc( - dict( - doctype="Quality Procedure", - quality_procedure_name="Test Procedure 1", - processes=[dict(process_description="Test Step 1")], - ) - ).insert() - - frappe.local.form_dict = frappe._dict( - doctype="Quality Procedure", - quality_procedure_name="Test Child 1", - parent_quality_procedure=procedure.name, - cmd="test", - is_root="false", - ) - node = add_node() - - procedure.reload() - - self.assertEqual(procedure.is_group, 1) - - # child row created - self.assertTrue([d for d in procedure.processes if d.procedure == node.name]) - - node.delete() - procedure.reload() - - # child unset - self.assertFalse([d for d in procedure.processes if d.name == node.name]) - - finally: - procedure.delete() - - -def create_procedure(): - return frappe.get_doc( - dict( - doctype="Quality Procedure", - quality_procedure_name="Test Procedure 1", - is_group=1, - processes=[dict(process_description="Test Step 1")], + procedure = create_procedure( + { + "quality_procedure_name": "Test Procedure 1", + "is_group": 1, + "processes": [dict(process_description="Test Step 1")], + } ) - ).insert() + + frappe.local.form_dict = frappe._dict( + doctype="Quality Procedure", + quality_procedure_name="Test Child 1", + parent_quality_procedure=procedure.name, + cmd="test", + is_root="false", + ) + node = add_node() + + procedure.reload() + + self.assertEqual(procedure.is_group, 1) + + # child row created + self.assertTrue([d for d in procedure.processes if d.procedure == node.name]) + + node.delete() + procedure.reload() + + # child unset + self.assertFalse([d for d in procedure.processes if d.name == node.name]) + + def test_remove_parent_from_old_child(self): + child_qp = create_procedure( + { + "quality_procedure_name": "Test Child 1", + "is_group": 0, + } + ) + group_qp = create_procedure( + { + "quality_procedure_name": "Test Group", + "is_group": 1, + "processes": [dict(procedure=child_qp.name)], + } + ) + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, group_qp.name) + + group_qp.reload() + del group_qp.processes[0] + group_qp.save() + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, None) + + def remove_child_from_old_parent(self): + child_qp = create_procedure( + { + "quality_procedure_name": "Test Child 1", + "is_group": 0, + } + ) + group_qp = create_procedure( + { + "quality_procedure_name": "Test Group", + "is_group": 1, + "processes": [dict(procedure=child_qp.name)], + } + ) + + group_qp.reload() + self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name]) + + child_qp.reload() + self.assertEqual(child_qp.parent_quality_procedure, group_qp.name) + + child_qp.parent_quality_procedure = None + child_qp.save() + + group_qp.reload() + self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name]) + + +def create_procedure(kwargs=None): + kwargs = frappe._dict(kwargs or {}) + + doc = frappe.new_doc("Quality Procedure") + doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure" + doc.is_group = kwargs.is_group or 0 + + for process in kwargs.processes or []: + doc.append("processes", process) + + doc.insert() + + return doc diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 18bb4b9bb46..87e5c5d0eec 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -131,7 +131,7 @@ "label": "Customer Type", "oldfieldname": "customer_type", "oldfieldtype": "Select", - "options": "Company\nIndividual", + "options": "Company\nIndividual\nProprietorship\nPartnership", "reqd": 1 }, { @@ -568,7 +568,7 @@ "link_fieldname": "party" } ], - "modified": "2023-05-09 15:38:40.255193", + "modified": "2023-10-19 16:56:27.327035", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -655,4 +655,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 18fd8ff8ef7..20d6268d739 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -541,6 +541,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, primary_action={ "label": "Send Email", "server_action": "erpnext.selling.doctype.customer.customer.send_emails", + "hide_on_success": True, "args": { "customer": customer, "customer_outstanding": customer_outstanding, diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index f5e641dceb4..555dc68f4eb 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -137,6 +137,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -668,7 +669,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-09-27 14:02:12.332407", + "modified": "2023-11-14 18:24:24.619832", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", @@ -678,4 +679,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4f498fb20d5..490bd7a9830 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1631,7 +1631,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-04-20 11:14:01.036202", + "modified": "2023-10-18 12:41:54.813462", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 25ea9189553..914adbb7906 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -204,7 +204,15 @@ class SalesOrder(SellingController): def validate_with_previous_doc(self): super(SalesOrder, self).validate_with_previous_doc( - {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} + { + "Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}, + "Quotation Item": { + "ref_dn_field": "quotation_item", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } ) if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 799ad555a52..ec400161e3a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1893,10 +1893,10 @@ class TestSalesOrder(FrappeTestCase): si.submit() pe.load_from_db() - self.assertEqual(pe.references[0].reference_name, si.name) - self.assertEqual(pe.references[0].allocated_amount, 200) - self.assertEqual(pe.references[1].reference_name, so.name) - self.assertEqual(pe.references[1].allocated_amount, 300) + self.assertEqual(pe.references[0].reference_name, so.name) + self.assertEqual(pe.references[0].allocated_amount, 300) + self.assertEqual(pe.references[1].reference_name, si.name) + self.assertEqual(pe.references[1].allocated_amount, 200) def test_delivered_item_material_request(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index a3e9977244c..bf28b2054d5 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -176,6 +176,7 @@ "print_hide": 1 }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -873,7 +874,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-28 14:56:42.031636", + "modified": "2023-11-14 18:37:12.787893", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -884,4 +885,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 46bdcfa5f15..85ce8a0d022 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -25,7 +25,7 @@ "so_required", "dn_required", "sales_update_frequency", - "over_order_allowance", + "blanket_order_allowance", "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", @@ -182,17 +182,17 @@ "fieldtype": "Check", "label": "Allow Sales Order Creation For Expired Quotation" }, - { - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "fieldname": "over_order_allowance", - "fieldtype": "Float", - "label": "Over Order Allowance (%)" - }, { "default": "0", "fieldname": "allow_negative_rates_for_items", "fieldtype": "Check", "label": "Allow Negative rates for Items" + }, + { + "description": "Percentage you are allowed to sell beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -200,7 +200,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-14 20:33:05.693667", + "modified": "2023-10-25 14:03:03.966701", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index 5dfc1db0976..ecb63d890a7 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -80,7 +80,7 @@ def get_data(filters=None): territory_orders = [] if t_quotation_names and sales_orders: - list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) + territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) t_order_names = [] if territory_orders: t_order_names = [t.name for t in territory_orders] diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 6aa400a53c7..589b3fe8b98 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -40,7 +40,7 @@ frappe.ui.form.on("Company", { filters:{ 'warehouse_type' : 'Transit', 'is_group': 0, - 'company': frm.doc.company + 'company': frm.doc.company_name } }; }); @@ -223,7 +223,6 @@ erpnext.company.setup_queries = function(frm) { ["cost_center", {}], ["round_off_cost_center", {}], ["depreciation_cost_center", {}], - ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], @@ -236,8 +235,6 @@ erpnext.company.setup_queries = function(frm) { $.each([ ["stock_adjustment_account", {"root_type": "Expense", "account_type": "Stock Adjustment"}], - ["expenses_included_in_valuation", - {"root_type": "Expense", "account_type": "Expenses Included in Valuation"}], ["stock_received_but_not_billed", {"root_type": "Liability", "account_type": "Stock Received But Not Billed"}], ["service_received_but_not_billed", diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 38f1c0bcdb2..349c328c06c 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -79,12 +79,10 @@ "column_break_32", "stock_received_but_not_billed", "default_provisional_account", - "expenses_included_in_valuation", "fixed_asset_defaults", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", - "expenses_included_in_asset_valuation", "column_break_40", "disposal_account", "depreciation_cost_center", @@ -466,14 +464,6 @@ "no_copy": 1, "options": "Account" }, - { - "fieldname": "expenses_included_in_valuation", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Expenses Included In Valuation", - "no_copy": 1, - "options": "Account" - }, { "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", @@ -493,12 +483,6 @@ "fieldtype": "Data", "label": "Series for Asset Depreciation Entry (Journal Entry)" }, - { - "fieldname": "expenses_included_in_asset_valuation", - "fieldtype": "Link", - "label": "Expenses Included In Asset Valuation", - "options": "Account" - }, { "fieldname": "column_break_40", "fieldtype": "Column Break" @@ -728,7 +712,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2023-07-07 05:41:41.537256", + "modified": "2023-10-23 10:19:24.322898", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index fcdf245659b..bea76f19412 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -92,7 +92,6 @@ class Company(NestedSet): ["Default Income Account", "default_income_account"], ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"], - ["Expense Included In Valuation Account", "expenses_included_in_valuation"], ] for account in accounts: @@ -384,7 +383,6 @@ class Company(NestedSet): "depreciation_expense_account": "Depreciation", "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", - "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", "default_expense_account": "Cost of Goods Sold", } @@ -394,7 +392,6 @@ class Company(NestedSet): "stock_received_but_not_billed": "Stock Received But Not Billed", "default_inventory_account": "Stock", "stock_adjustment_account": "Stock Adjustment", - "expenses_included_in_valuation": "Expenses Included In Valuation", } ) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 39a215f3831..efc3fd1d33d 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -81,8 +81,10 @@ frappe.ui.form.on("Employee", { employee: frm.doc.name, email: frm.doc.prefered_email }, + freeze: true, + freeze_message: __("Creating User..."), callback: function (r) { - frm.set_value("user_id", r.message); + frm.reload_doc(); } }); } diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 78f2e4935e7..a632c90f51e 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -49,6 +49,9 @@ class Employee(NestedSet): else: existing_user_id = frappe.db.get_value("Employee", self.name, "user_id") if existing_user_id: + user = frappe.get_doc("User", existing_user_id) + validate_employee_role(user, ignore_emp_check=True) + user.save(ignore_permissions=True) remove_user_permission("Employee", self.name, existing_user_id) def after_rename(self, old, new, merge): @@ -254,12 +257,26 @@ class Employee(NestedSet): frappe.cache().hdel("employees_with_number", prev_number) -def validate_employee_role(doc, method): +def validate_employee_role(doc, method=None, ignore_emp_check=False): # called via User hook - if "Employee" in [d.role for d in doc.get("roles")]: - if not frappe.db.get_value("Employee", {"user_id": doc.name}): - frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role")) - doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0]) + if not ignore_emp_check: + if frappe.db.get_value("Employee", {"user_id": doc.name}): + return + + user_roles = [d.role for d in doc.get("roles")] + if "Employee" in user_roles: + frappe.msgprint( + _("User {0}: Removed Employee role as there is no mapped employee.").format(doc.name) + ) + doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0]) + + if "Employee Self Service" in user_roles: + frappe.msgprint( + _("User {0}: Removed Employee Self Service role as there is no mapped employee.").format( + doc.name + ) + ) + doc.get("roles").remove(doc.get("roles", {"role": "Employee Self Service"})[0]) def update_user_permissions(doc, method): @@ -371,6 +388,8 @@ def create_user(employee, user=None, email=None): } ) user.insert() + emp.user_id = user.name + emp.save() return user.name diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index 5a693c5effb..9b706836269 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -25,6 +25,15 @@ class TestEmployee(unittest.TestCase): employee1_doc.status = "Left" self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) + def test_user_has_employee(self): + employee = make_employee("test_emp_user_creation@company.com") + employee_doc = frappe.get_doc("Employee", employee) + user = employee_doc.user_id + self.assertTrue("Employee" in frappe.get_roles(user)) + employee_doc.user_id = "" + employee_doc.save() + self.assertTrue("Employee" not in frappe.get_roles(user)) + def tearDown(self): frappe.db.rollback() diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 481a3a5ebea..d266285b29a 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -108,7 +108,16 @@ class TransactionDeletionRecord(Document): if no_of_docs > 0: self.delete_version_log(docfield["parent"], docfield["fieldname"]) - self.delete_communications(docfield["parent"], docfield["fieldname"]) + + reference_docs = frappe.get_all( + docfield["parent"], filters={docfield["fieldname"]: self.company} + ) + reference_doc_names = [r.name for r in reference_docs] + + self.delete_communications(docfield["parent"], reference_doc_names) + self.delete_comments(docfield["parent"], reference_doc_names) + self.unlink_attachments(docfield["parent"], reference_doc_names) + self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) self.delete_child_tables(docfield["parent"], docfield["fieldname"]) @@ -197,19 +206,49 @@ class TransactionDeletionRecord(Document): (versions.ref_doctype == doctype) & (versions.docname.isin(batch)) ).run() - def delete_communications(self, doctype, company_fieldname): - reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) - reference_doc_names = [r.name for r in reference_docs] - + def delete_communications(self, doctype, reference_doc_names): communications = frappe.get_all( "Communication", filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, ) communication_names = [c.name for c in communications] + if not communication_names: + return + for batch in create_batch(communication_names, self.batch_size): frappe.delete_doc("Communication", batch, ignore_permissions=True) + def delete_comments(self, doctype, reference_doc_names): + comments = frappe.get_all( + "Comment", + filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, + ) + comment_names = [c.name for c in comments] + + if not comment_names: + return + + for batch in create_batch(comment_names, self.batch_size): + frappe.delete_doc("Comment", batch, ignore_permissions=True) + + def unlink_attachments(self, doctype, reference_doc_names): + files = frappe.get_all( + "File", + filters={"attached_to_doctype": doctype, "attached_to_name": ["in", reference_doc_names]}, + ) + file_names = [c.name for c in files] + + if not file_names: + return + + file = qb.DocType("File") + + for batch in create_batch(file_names, self.batch_size): + qb.update(file).set(file.attached_to_doctype, None).set(file.attached_to_name, None).where( + file.name.isin(batch) + ).run() + @frappe.whitelist() def get_doctypes_to_be_ignored(): diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 967c5729bf4..4bc14a976d6 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -61,6 +61,7 @@ "oldfieldname": "item", "oldfieldtype": "Link", "options": "Item", + "read_only_depends_on": "eval:!doc.__islocal", "reqd": 1 }, { @@ -207,7 +208,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2022-02-21 08:08:23.999236", + "modified": "2023-11-09 12:17:28.339975", "modified_by": "Administrator", "module": "Stock", "name": "Batch", diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index a11572776a2..02684a72419 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -5,20 +5,23 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "warehouse", "item_code", - "reserved_qty", + "column_break_yreo", + "warehouse", + "section_break_stag", "actual_qty", - "ordered_qty", - "indented_qty", "planned_qty", - "projected_qty", + "indented_qty", + "ordered_qty", + "column_break_xn5j", + "reserved_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", "reserved_qty_for_production_plan", - "ma_rate", + "projected_qty", + "section_break_pmrs", "stock_uom", - "fcfs_rate", + "column_break_0slj", "valuation_rate", "stock_value" ], @@ -56,7 +59,7 @@ "fieldname": "reserved_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Reserved Quantity", + "label": "Reserved Qty", "oldfieldname": "reserved_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -67,7 +70,7 @@ "fieldtype": "Float", "in_filter": 1, "in_list_view": 1, - "label": "Actual Quantity", + "label": "Actual Qty", "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -77,7 +80,7 @@ "fieldname": "ordered_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Ordered Quantity", + "label": "Ordered Qty", "oldfieldname": "ordered_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -86,7 +89,7 @@ "default": "0.00", "fieldname": "indented_qty", "fieldtype": "Float", - "label": "Requested Quantity", + "label": "Requested Qty", "oldfieldname": "indented_qty", "oldfieldtype": "Currency", "read_only": 1 @@ -116,20 +119,9 @@ { "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "label": "Reserved Qty for sub contract", + "label": "Reserved Qty for Subcontract", "read_only": 1 }, - { - "fieldname": "ma_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "Moving Average Rate", - "oldfieldname": "ma_rate", - "oldfieldtype": "Currency", - "print_hide": 1, - "read_only": 1, - "report_hide": 1 - }, { "fieldname": "stock_uom", "fieldtype": "Link", @@ -140,17 +132,6 @@ "options": "UOM", "read_only": 1 }, - { - "fieldname": "fcfs_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "FCFS Rate", - "oldfieldname": "fcfs_rate", - "oldfieldtype": "Currency", - "print_hide": 1, - "read_only": 1, - "report_hide": 1 - }, { "fieldname": "valuation_rate", "fieldtype": "Float", @@ -172,13 +153,33 @@ "fieldtype": "Float", "label": "Reserved Qty for Production Plan", "read_only": 1 + }, + { + "fieldname": "section_break_stag", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_yreo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_xn5j", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_pmrs", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_0slj", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "in_create": 1, "links": [], - "modified": "2023-05-02 23:26:21.806965", + "modified": "2023-11-01 15:35:51.722534", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 5abea9e69fe..bd6d7241c2b 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -34,10 +34,15 @@ class Bin(Document): get_reserved_qty_for_production_plan, ) - self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( + reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( self.item_code, self.warehouse ) + if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan: + return + + self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) + self.db_set( "reserved_qty_for_production_plan", flt(self.reserved_qty_for_production_plan), @@ -48,6 +53,29 @@ class Bin(Document): self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_qty_for_for_sub_assembly(self): + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_sub_assembly, + ) + + reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly( + self.item_code, self.warehouse + ) + + if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan: + return + + self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan) + self.set_projected_qty() + + self.db_set( + { + "projected_qty": self.projected_qty, + "reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan), + }, + update_modified=True, + ) + def update_reserved_qty_for_production(self): """Update qty reserved for production from Production Item tables in open work orders""" diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json index 225da6d15ec..0c4757ffadb 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json @@ -103,15 +103,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Closing Stock Balance", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "include_uom", "fieldtype": "Link", @@ -145,4 +136,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index c6f3197a668..65828f3a4ac 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1056,6 +1056,7 @@ class TestDeliveryNote(FrappeTestCase): dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3) si1 = make_sales_invoice(dn1.name) + si1.update_billed_amount_in_delivery_note = True si1.insert() si1.submit() dn1.reload() @@ -1064,6 +1065,7 @@ class TestDeliveryNote(FrappeTestCase): dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4) si2 = make_sales_invoice(dn2.name) + si2.update_billed_amount_in_delivery_note = True si2.insert() si2.submit() dn2.reload() @@ -1315,6 +1317,21 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + def test_non_internal_transfer_delivery_note(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + dn = create_delivery_note(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company) + dn.items[0].db_set("target_warehouse", warehouse) + + dn.reload() + + self.assertEqual(dn.items[0].target_warehouse, warehouse) + + dn.save() + dn.reload() + self.assertFalse(dn.items[0].target_warehouse) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 237088f64a7..b85bfe5036d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -165,6 +165,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -869,7 +870,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-16 16:18:18.013379", + "modified": "2023-11-14 18:37:38.638144", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json index 5610a8108ab..42560e612ef 100644 --- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json +++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json @@ -1,815 +1,197 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-10-16 16:46:28.166950", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-16 16:46:28.166950", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "address", + "locked", + "column_break_6", + "customer_address", + "visited", + "order_information_section", + "delivery_note", + "cb_order", + "grand_total", + "section_break_7", + "contact", + "email_sent_to", + "column_break_7", + "customer_contact", + "section_break_9", + "distance", + "estimated_arrival", + "lat", + "column_break_19", + "uom", + "lng", + "more_information_section", + "details" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Address Name", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Address Name", + "options": "Address", + "print_hide": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lock", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Lock", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "locked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Locked" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_address", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_address", + "fieldtype": "Small Text", + "label": "Customer Address", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "visited", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Visited", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "default": "0", + "depends_on": "eval:doc.docstatus==1", + "fieldname": "visited", + "fieldtype": "Check", + "label": "Visited", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "order_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Order Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "order_information_section", + "fieldtype": "Section Break", + "label": "Order Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "delivery_note", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Delivery Note", - "length": 0, - "no_copy": 1, - "options": "Delivery Note", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "no_copy": 1, + "options": "Delivery Note", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_order", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "cb_order", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Grand Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "grand_total", + "fieldtype": "Currency", + "label": "Grand Total", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Contact Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Name", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact", + "fieldtype": "Link", + "label": "Contact Name", + "options": "Contact", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email_sent_to", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email sent to", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "email_sent_to", + "fieldtype": "Data", + "label": "Email sent to", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_contact", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "customer_contact", + "fieldtype": "Small Text", + "label": "Customer Contact", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dispatch Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Dispatch Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "distance", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Distance", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "distance", + "fieldtype": "Float", + "label": "Distance", + "precision": "2", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "estimated_arrival", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Arrival", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "estimated_arrival", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Estimated Arrival" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lat", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Latitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "lat", + "fieldtype": "Float", + "hidden": 1, + "label": "Latitude" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_19", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "depends_on": "eval:doc.distance", - "fieldname": "uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.distance", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lng", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Longitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "lng", + "fieldtype": "Float", + "hidden": 1, + "label": "Longitude" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "more_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "details", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "details", + "fieldtype": "Text Editor", + "label": "Details" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-10-16 05:23:25.661542", - "modified_by": "Administrator", - "module": "Stock", - "name": "Delivery Stop", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-09-29 09:22:53.435161", + "modified_by": "Administrator", + "module": "Stock", + "name": "Delivery Stop", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index a6fbb66aa2b..4ba5b32ca62 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -62,8 +62,13 @@ frappe.ui.form.on('Delivery Trip', { company: frm.doc.company, } }) - }, __("Get customers from")); + }, __("Get stops from")); } + frm.add_custom_button(__("Delivery Notes"), function () { + frappe.set_route("List", "Delivery Note", + {'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]} + ); + }, __("View")); }, calculate_arrival_time: function (frm) { diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index af2f4113e1e..c531a8769c0 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -170,7 +170,7 @@ class DeliveryTrip(Document): for stop in self.delivery_stops: leg.append(stop.customer_address) - if optimize and stop.lock: + if optimize and stop.locked: route_list.append(leg) leg = [stop.customer_address] diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index ed699e37b88..9b8b46e6e0a 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -46,7 +46,7 @@ class TestDeliveryTrip(FrappeTestCase): self.assertEqual(len(route_list[0]), 4) def test_unoptimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=False) @@ -65,7 +65,7 @@ class TestDeliveryTrip(FrappeTestCase): self.assertEqual(len(route_list[0]), 4) def test_optimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=True) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 693d33ffb71..e1dd481b075 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -280,7 +280,7 @@ class Item(Document): # add item taxes from template for d in template.get("taxes"): - self.append("taxes", {"item_tax_template": d.item_tax_template}) + self.append("taxes", d) # copy re-order table if empty if not self.get("reorder_levels"): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 53f6b7f8f17..9aa66a9a1ec 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -893,6 +893,8 @@ def create_item( opening_stock=0, is_fixed_asset=0, asset_category=None, + buying_cost_center=None, + selling_cost_center=None, company="_Test Company", ): if not frappe.db.exists("Item", item_code): @@ -910,7 +912,15 @@ def create_item( item.is_purchase_item = is_purchase_item item.is_customer_provided_item = is_customer_provided_item item.customer = customer or "" - item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) + item.append( + "item_defaults", + { + "default_warehouse": warehouse, + "company": company, + "selling_cost_center": selling_cost_center, + "buying_cost_center": buying_cost_center, + }, + ) item.save() else: item = frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index ce489ff52b4..8a4b4eef0ae 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", { frm.set_query("item_code", function() { return { filters: { - "disabled": 0, "has_variants": 0 } }; diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 311b0ed8ce9..659bc42f0a6 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -405,6 +405,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): ["uom", "uom"], ["sales_order", "sales_order"], ["sales_order_item", "sales_order_item"], + ["wip_composite_asset", "wip_composite_asset"], ], "postprocess": update_item, "condition": select_item, diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 770dacdd19c..ed4a7e7cf64 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -37,6 +37,10 @@ "rate", "col_break3", "amount", + "accounting_details_section", + "expense_account", + "column_break_glru", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -50,11 +54,10 @@ "lead_time_date", "sales_order", "sales_order_item", + "col_break4", "production_plan", "material_request_plan_item", "job_card_item", - "col_break4", - "expense_account", "section_break_46", "page_break" ], @@ -108,6 +111,7 @@ "width": "250px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach Image", "label": "Image", @@ -455,13 +459,28 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_glru", + "fieldtype": "Column Break" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-07 20:23:31.250252", + "modified": "2023-11-14 18:37:59.599115", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index da534b245c6..10bd335468d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -13,7 +13,6 @@ from pypika import functions as fn import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled -from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction @@ -146,8 +145,8 @@ class PurchaseReceipt(BuyingController): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account - arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account( + self.get_company_default("asset_received_but_not_billed") + get_asset_account( "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company ) break @@ -315,7 +314,7 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_tax_gl_entries(gl_entries) - self.get_asset_gl_entry(gl_entries) + update_regional_gl_entries(gl_entries, self) return process_gl_map(gl_entries) @@ -324,14 +323,6 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) - stock_rbnb = None - if erpnext.is_perpetual_inventory_enabled(self.company): - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - landed_cost_entries = get_item_account_wise_additional_cost(self.name) - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - - warehouse_with_no_account = [] - stock_items = self.get_stock_items() provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" @@ -340,28 +331,258 @@ class PurchaseReceipt(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) + def validate_account(account_type): + frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type)) + + def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name): + account_currency = get_account_currency(stock_asset_account_name) + + if not stock_asset_account_name: + validate_account("Asset or warehouse account") + + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_asset_account_name, + cost_center=d.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_asset_rbnb, + account_currency=account_currency, + item=item, + ) + + def make_stock_received_but_not_billed_entry(item): + account = ( + warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb + ) + account_currency = get_account_currency(account) + + # GL Entry for from warehouse or Stock Received but not billed + # Intentionally passed negative debit amount to avoid incorrect GL Entry validation + credit_amount = ( + flt(item.base_net_amount, item.precision("base_net_amount")) + if account_currency == self.company_currency + else flt(item.net_amount, item.precision("net_amount")) + ) + + outgoing_amount = item.base_net_amount + if self.is_internal_transfer() and item.valuation_rate: + outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) + credit_amount = outgoing_amount + + if credit_amount: + if not account: + validate_account("Stock or Asset Received But Not Billed") + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")), + credit=0.0, + remarks=remarks, + against_account=stock_asset_account_name, + debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")), + account_currency=account_currency, + item=item, + ) + + # check if the exchange rate has changed + if d.get("purchase_invoice"): + if ( + exchange_rate_map[item.purchase_invoice] + and self.conversion_rate != exchange_rate_map[item.purchase_invoice] + and item.net_rate == net_rate_map[item.purchase_invoice_item] + ): + + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * ( + exchange_rate_map[item.purchase_invoice] - self.conversion_rate + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=0.0, + credit=discrepancy_caused_by_exchange_rate_difference, + remarks=remarks, + against_account=self.supplier, + debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=account_currency, + item=item, + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=self.get_company_default("exchange_gain_loss_account"), + cost_center=d.cost_center, + debit=discrepancy_caused_by_exchange_rate_difference, + credit=0.0, + remarks=remarks, + against_account=self.supplier, + debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=account_currency, + item=item, + ) + + return outgoing_amount + + def make_landed_cost_gl_entries(item): + # Amount added through landed-cost-voucher + if item.landed_cost_voucher_amount and landed_cost_entries: + if (item.item_code, item.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): + account_currency = get_account_currency(account) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) + + if not account: + validate_account("Landed Cost Account") + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=item.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=stock_asset_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=item.project, + item=item, + ) + + def make_rate_difference_entry(item): + if item.rate_difference_with_purchase_invoice and stock_asset_rbnb: + account_currency = get_account_currency(stock_asset_rbnb) + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_asset_rbnb, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rate_difference_with_purchase_invoice), + remarks=_("Adjustment based on Purchase Invoice rate"), + against_account=stock_asset_account_name, + account_currency=account_currency, + project=item.project, + item=item, + ) + + def make_sub_contracting_gl_entries(item): + # sub-contracting warehouse + if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rm_supp_cost), + remarks=remarks, + against_account=stock_asset_account_name, + account_currency=supplier_warehouse_account_currency, + item=item, + ) + + def make_divisional_loss_gl_entry(item, outgoing_amount): + if item.is_fixed_asset: + return + + # divisional loss adjustment + valuation_amount_as_per_doc = ( + flt(outgoing_amount, d.precision("base_net_amount")) + + flt(item.landed_cost_voucher_amount) + + flt(item.rm_supp_cost) + + flt(item.item_tax_amount) + + flt(item.rate_difference_with_purchase_invoice) + ) + + divisional_loss = flt( + valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount") + ) + + if divisional_loss: + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) + or stock_asset_rbnb + ) + + cost_center = item.cost_center or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) + account_currency = get_account_currency(loss_account) + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=stock_asset_account_name, + account_currency=account_currency, + project=item.project, + item=item, + ) + + stock_items = self.get_stock_items() + warehouse_with_no_account = [] + for d in self.get("items"): - if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): - if warehouse_account.get(d.warehouse): - stock_value_diff = frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": self.name, - "voucher_detail_no": d.name, - "warehouse": d.warehouse, - "is_cancelled": 0, - }, - "stock_value_difference", + if ( + provisional_accounting_for_non_stock_items + and d.item_code not in stock_items + and flt(d.qty) + and d.get("provisional_expense_account") + and not d.is_fixed_asset + ): + self.add_provisional_gl_entry( + d, gl_entries, self.posting_date, d.get("provisional_expense_account") + ) + elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return): + remarks = self.get("remarks") or _("Accounting Entry for {0}").format( + "Asset" if d.is_fixed_asset else "Stock" + ) + + if not ( + (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items) + or d.is_fixed_asset + ): + continue + + stock_asset_rbnb = ( + self.get_company_default("asset_received_but_not_billed") + if d.is_fixed_asset + else self.get_company_default("stock_received_but_not_billed") + ) + landed_cost_entries = get_item_account_wise_additional_cost(self.name) + + if d.is_fixed_asset: + account_type = ( + "capital_work_in_progress_account" + if is_cwip_accounting_enabled(d.asset_category) + else "fixed_asset_account" ) - warehouse_account_name = warehouse_account[d.warehouse]["account"] - warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] + stock_asset_account_name = get_asset_account( + account_type, asset_category=d.asset_category, company=self.company + ) + + stock_value_diff = ( + flt(d.net_amount) + + flt(d.item_tax_amount / self.conversion_rate) + + flt(d.landed_cost_voucher_amount) + ) + elif warehouse_account.get(d.warehouse): + stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) + stock_asset_account_name = warehouse_account[d.warehouse]["account"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( "account_currency" ) - remarks = self.get("remarks") or _("Accounting Entry for Stock") # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, @@ -369,214 +590,24 @@ class PurchaseReceipt(BuyingController): if ( flt(stock_value_diff) == flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse) - and warehouse_account_name == supplier_warehouse_account + and stock_asset_account_name == supplier_warehouse_account ): continue - self.add_gl_entry( - gl_entries=gl_entries, - account=warehouse_account_name, - cost_center=d.cost_center, - debit=stock_value_diff, - credit=0.0, - remarks=remarks, - against_account=stock_rbnb, - account_currency=warehouse_account_currency, - item=d, - ) - - # GL Entry for from warehouse or Stock Received but not billed - # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - credit_currency = ( - get_account_currency(warehouse_account[d.from_warehouse]["account"]) - if d.from_warehouse - else get_account_currency(stock_rbnb) - ) - - credit_amount = ( - flt(d.base_net_amount, d.precision("base_net_amount")) - if credit_currency == self.company_currency - else flt(d.net_amount, d.precision("net_amount")) - ) - - outgoing_amount = d.base_net_amount - if self.is_internal_transfer() and d.valuation_rate: - outgoing_amount = abs( - frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": self.name, - "voucher_detail_no": d.name, - "warehouse": d.from_warehouse, - "is_cancelled": 0, - }, - "stock_value_difference", - ) - ) - credit_amount = outgoing_amount - - if credit_amount: - account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")), - credit=0.0, - remarks=remarks, - against_account=warehouse_account_name, - debit_in_account_currency=-1 * credit_amount, - account_currency=credit_currency, - item=d, - ) - - # check if the exchange rate has changed - if d.get("purchase_invoice"): - if ( - exchange_rate_map[d.purchase_invoice] - and self.conversion_rate != exchange_rate_map[d.purchase_invoice] - and d.net_rate == net_rate_map[d.purchase_invoice_item] - ): - - discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * ( - exchange_rate_map[d.purchase_invoice] - self.conversion_rate - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=discrepancy_caused_by_exchange_rate_difference, - remarks=remarks, - against_account=self.supplier, - debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, - item=d, - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=self.get_company_default("exchange_gain_loss_account"), - cost_center=d.cost_center, - debit=discrepancy_caused_by_exchange_rate_difference, - credit=0.0, - remarks=remarks, - against_account=self.supplier, - debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, - item=d, - ) - - # Amount added through landed-cos-voucher - if d.landed_cost_voucher_amount and landed_cost_entries: - if (d.item_code, d.name) in landed_cost_entries: - for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=warehouse_account_name, - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=d.project, - item=d, - ) - - if d.rate_difference_with_purchase_invoice and stock_rbnb: - account_currency = get_account_currency(stock_rbnb) - self.add_gl_entry( - gl_entries=gl_entries, - account=stock_rbnb, - cost_center=d.cost_center, - debit=0.0, - credit=flt(d.rate_difference_with_purchase_invoice), - remarks=_("Adjustment based on Purchase Invoice rate"), - against_account=warehouse_account_name, - account_currency=account_currency, - project=d.project, - item=d, - ) - - # sub-contracting warehouse - if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): - self.add_gl_entry( - gl_entries=gl_entries, - account=supplier_warehouse_account, - cost_center=d.cost_center, - debit=0.0, - credit=flt(d.rm_supp_cost), - remarks=remarks, - against_account=warehouse_account_name, - account_currency=supplier_warehouse_account_currency, - item=d, - ) - - # divisional loss adjustment - valuation_amount_as_per_doc = ( - flt(outgoing_amount, d.precision("base_net_amount")) - + flt(d.landed_cost_voucher_amount) - + flt(d.rm_supp_cost) - + flt(d.item_tax_amount) - + flt(d.rate_difference_with_purchase_invoice) - ) - - divisional_loss = flt( - valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount") - ) - - if divisional_loss: - if self.is_return or flt(d.item_tax_amount): - loss_account = expenses_included_in_valuation - else: - loss_account = ( - self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb - ) - - cost_center = d.cost_center or frappe.get_cached_value( - "Company", self.company, "cost_center" - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=loss_account, - cost_center=cost_center, - debit=divisional_loss, - credit=0.0, - remarks=remarks, - against_account=warehouse_account_name, - account_currency=credit_currency, - project=d.project, - item=d, - ) - - elif ( - d.warehouse not in warehouse_with_no_account - or d.rejected_warehouse not in warehouse_with_no_account - ): - warehouse_with_no_account.append(d.warehouse) - elif ( - d.item_code not in stock_items - and not d.is_fixed_asset - and flt(d.qty) - and provisional_accounting_for_non_stock_items - and d.get("provisional_expense_account") + if (flt(d.valuation_rate) or self.is_return or d.is_fixed_asset) and flt(d.qty): + make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) + outgoing_amount = make_stock_received_but_not_billed_entry(d) + make_landed_cost_gl_entries(d) + make_rate_difference_entry(d) + make_sub_contracting_gl_entries(d) + make_divisional_loss_gl_entry(d, outgoing_amount) + elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( + d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account ): - self.add_provisional_gl_entry( - d, gl_entries, self.posting_date, d.get("provisional_expense_account") - ) + warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) + + if d.is_fixed_asset: + self.update_assets(d, d.valuation_rate) if warehouse_with_no_account: frappe.msgprint( @@ -589,8 +620,8 @@ class PurchaseReceipt(BuyingController): self, item, gl_entries, posting_date, provisional_account, reverse=0 ): credit_currency = get_account_currency(provisional_account) - debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account + debit_currency = get_account_currency(item.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") multiplication_factor = 1 @@ -631,11 +662,8 @@ class PurchaseReceipt(BuyingController): ) def make_tax_gl_entries(self, gl_entries): - - if erpnext.is_perpetual_inventory_enabled(self.company): - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) + is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): @@ -655,26 +683,26 @@ class PurchaseReceipt(BuyingController): if negative_expense_to_be_booked and valuation_tax: # Backward compatibility: - # If expenses_included_in_valuation account has been credited in against PI # and charges added via Landed Cost Voucher, # post valuation related charges on "Stock Received But Not Billed" - # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account - - negative_expense_booked_in_pi = frappe.db.sql( - """select name from `tabPurchase Invoice Item` pi - where docstatus = 1 and purchase_receipt=%s - and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", - (self.name, expenses_included_in_valuation), - ) - against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked - stock_rbnb = self.get_company_default("stock_received_but_not_billed") + stock_rbnb = ( + self.get("asset_received_but_not_billed") + if is_asset_pr + else self.get_company_default("stock_received_but_not_billed") + ) i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi + where docstatus = 1 and purchase_receipt=%s + and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' + and voucher_no=pi.parent and account=%s)""", + (self.name, tax.account_head), + ) if negative_expense_booked_in_pi: account = stock_rbnb @@ -702,103 +730,6 @@ class PurchaseReceipt(BuyingController): i += 1 - def get_asset_gl_entry(self, gl_entries): - for item in self.get("items"): - if item.is_fixed_asset: - if is_cwip_accounting_enabled(item.asset_category): - self.add_asset_gl_entries(item, gl_entries) - if flt(item.landed_cost_voucher_amount): - self.add_lcv_gl_entries(item, gl_entries) - # update assets gross amount by its valuation rate - # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item - self.update_assets(item, item.valuation_rate) - return gl_entries - - def add_asset_gl_entries(self, item, gl_entries): - arbnb_account = self.get_company_default("asset_received_but_not_billed") - # This returns category's cwip account if not then fallback to company's default cwip account - cwip_account = get_asset_account( - "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company - ) - - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) - base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - remarks = self.get("remarks") or _("Accounting Entry for Asset") - - cwip_account_currency = get_account_currency(cwip_account) - # debit cwip account - debit_in_account_currency = ( - base_asset_amount if cwip_account_currency == self.company_currency else asset_amount - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=cwip_account, - cost_center=item.cost_center, - debit=base_asset_amount, - credit=0.0, - remarks=remarks, - against_account=arbnb_account, - debit_in_account_currency=debit_in_account_currency, - item=item, - ) - - asset_rbnb_currency = get_account_currency(arbnb_account) - # credit arbnb account - credit_in_account_currency = ( - base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=arbnb_account, - cost_center=item.cost_center, - debit=0.0, - credit=base_asset_amount, - remarks=remarks, - against_account=cwip_account, - credit_in_account_currency=credit_in_account_currency, - item=item, - ) - - def add_lcv_gl_entries(self, item, gl_entries): - expenses_included_in_asset_valuation = self.get_company_default( - "expenses_included_in_asset_valuation" - ) - if not is_cwip_accounting_enabled(item.asset_category): - asset_account = get_asset_category_account( - asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company - ) - else: - # This returns company's default cwip account - asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) - - remarks = self.get("remarks") or _("Accounting Entry for Stock") - - self.add_gl_entry( - gl_entries=gl_entries, - account=expenses_included_in_asset_valuation, - cost_center=item.cost_center, - debit=0.0, - credit=flt(item.landed_cost_voucher_amount), - remarks=remarks, - against_account=asset_account, - project=item.project, - item=item, - ) - - self.add_gl_entry( - gl_entries=gl_entries, - account=asset_account, - cost_center=item.cost_center, - debit=flt(item.landed_cost_voucher_amount), - credit=0.0, - remarks=remarks, - against_account=expenses_included_in_asset_valuation, - project=item.project, - item=item, - ) - def update_assets(self, item, valuation_rate): assets = frappe.db.get_all( "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} @@ -823,16 +754,28 @@ class PurchaseReceipt(BuyingController): po_details.append(d.purchase_order_item) if po_details: - updated_pr += update_billed_amount_based_on_po(po_details, update_modified) + updated_pr += update_billed_amount_based_on_po(po_details, update_modified, self) for pr in set(updated_pr): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) - self.load_from_db() + +def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse): + return frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": voucher_no, + "voucher_detail_no": voucher_detail_no, + "warehouse": warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) -def update_billed_amount_based_on_po(po_details, update_modified=True): +def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None): po_billed_amt_details = get_billed_amount_against_po(po_details) # Get all Purchase Receipt Item rows against the Purchase Order Items @@ -861,13 +804,19 @@ def update_billed_amount_based_on_po(po_details, update_modified=True): po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po if pr_item.billed_amt != billed_amt_agianst_pr: - frappe.db.set_value( - "Purchase Receipt Item", - pr_item.name, - "billed_amt", - billed_amt_agianst_pr, - update_modified=update_modified, - ) + # update existing doc if possible + if pr_doc and pr_item.parent == pr_doc.name: + pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None) + pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + + else: + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) @@ -943,9 +892,6 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): - # Reload as billed amount was set in db directly - pr_doc.load_from_db() - # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -971,7 +917,6 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) - pr_doc.load_from_db() if update_modified: pr_doc.set_status(update=True) @@ -1093,6 +1038,7 @@ def make_purchase_invoice(source_name, target_doc=None): "is_fixed_asset": "is_fixed_asset", "asset_location": "asset_location", "asset_category": "asset_category", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "filter": lambda d: get_pending_qty(d)[0] <= 0 @@ -1250,3 +1196,8 @@ def get_item_account_wise_additional_cost(purchase_document): def on_doctype_update(): frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) + + +@erpnext.allow_regional +def update_regional_gl_entries(gl_list, doc): + return diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a93d5b1bbbe..404758ce94f 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -928,17 +928,33 @@ class TestPurchaseReceipt(FrappeTestCase): pr1.cancel() def test_stock_transfer_from_purchase_receipt(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + pr1 = make_purchase_receipt( - warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" + warehouse="Stores - TCP1", company="_Test Company with perpetual inventory" ) - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", ) - pr.supplier_warehouse = "" + pr = make_inter_company_purchase_receipt(dn1.name) pr.items[0].from_warehouse = "Work In Progress - TCP1" - + pr.items[0].warehouse = "Stores - TCP1" pr.submit() gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -955,6 +971,11 @@ class TestPurchaseReceipt(FrappeTestCase): pr1.cancel() def test_stock_transfer_from_purchase_receipt_with_valuation(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + create_warehouse( "_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", @@ -962,16 +983,28 @@ class TestPurchaseReceipt(FrappeTestCase): ) pr1 = make_purchase_receipt( - warehouse="_Test Warehouse for Valuation - TCP1", + warehouse="Stores - TCP1", company="_Test Company with perpetual inventory", ) - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=50, + warehouse="Stores - TCP1", + target_warehouse="_Test Warehouse for Valuation - TCP1", ) + pr = make_inter_company_purchase_receipt(dn1.name) pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" - pr.supplier_warehouse = "" + pr.items[0].warehouse = "Stores - TCP1" pr.append( "taxes", @@ -2203,6 +2236,21 @@ class TestPurchaseReceipt(FrappeTestCase): for entry in gl_entries: self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + def non_internal_transfer_purchase_receipt(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + pr_doc = make_purchase_receipt(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company) + pr_doc.items[0].db_set("target_warehouse", "warehouse") + + pr_doc.reload() + + self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name) + + pr_doc.save() + pr_doc.reload() + self.assertFalse(pr_doc.items[0].from_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/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 4911523e7ed..749dbf2111f 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -118,7 +118,9 @@ "dimension_col_break", "cost_center", "section_break_80", - "page_break" + "page_break", + "sales_order", + "sales_order_item" ], "fields": [ { @@ -184,6 +186,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -921,7 +924,8 @@ "label": "Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -1025,12 +1029,32 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "no_copy": 1, + "options": "Sales Order", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-03 21:11:50.547261", + "modified": "2023-11-14 18:38:15.251994", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 1853f45f583..aa90ff03a82 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, now, nowdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -200,6 +200,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): riv.set_status("Skipped") + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_prevention_of_cancelled_transaction_riv(self): frappe.flags.dont_execute_stock_reposts = True @@ -377,6 +378,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): accounts_settings.acc_frozen_upto = "" accounts_settings.save() + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_create_repost_entry_for_cancelled_document(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index a9989956ebb..71b2faa41de 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -191,7 +191,7 @@ class SerialNo(StockController): sle_dict = self.get_stock_ledger_entries(serial_no) if sle_dict: if sle_dict.get("incoming", []): - entries["purchase_sle"] = sle_dict["incoming"][0] + entries["purchase_sle"] = sle_dict["incoming"][-1] if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0: entries["last_sle"] = sle_dict["incoming"][0] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9560b52f59c..5031625a6b8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -168,7 +168,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage" ) ) self.queue_action("submit", timeout=2000) @@ -179,7 +179,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage" ) ) self.queue_action("cancel", timeout=2000) @@ -451,31 +451,37 @@ class StockEntry(StockController): item_code.append(item.item_code) def validate_fg_completed_qty(self): - item_wise_qty = {} - if self.purpose == "Manufacture" and self.work_order: - for d in self.items: - if d.is_finished_item: - if self.process_loss_qty: - d.qty = self.fg_completed_qty - self.process_loss_qty + if self.purpose != "Manufacture": + return - item_wise_qty.setdefault(d.item_code, []).append(d.qty) + fg_qty = defaultdict(float) + for d in self.items: + if d.is_finished_item: + fg_qty[d.item_code] += flt(d.qty) + + if not fg_qty: + return precision = frappe.get_precision("Stock Entry Detail", "qty") - for item_code, qty_list in item_wise_qty.items(): - total = flt(sum(qty_list), precision) + fg_item = list(fg_qty.keys())[0] + fg_item_qty = flt(fg_qty[fg_item], precision) + fg_completed_qty = flt(self.fg_completed_qty, precision) - if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty: - self.process_loss_qty = flt(self.fg_completed_qty - total, precision) - self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) + for d in self.items: + if not fg_qty.get(d.item_code): + continue - if self.process_loss_qty: - total += flt(self.process_loss_qty, precision) + if (fg_completed_qty - fg_item_qty) > 0: + self.process_loss_qty = fg_completed_qty - fg_item_qty - if self.fg_completed_qty != total: + if not self.process_loss_qty: + continue + + if fg_completed_qty != (flt(fg_item_qty) + flt(self.process_loss_qty, precision)): frappe.throw( - _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( - frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) - ) + _( + "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." + ).format(frappe.bold(self.process_loss_qty), frappe.bold(d.item_code)) ) def validate_difference_account(self): @@ -982,14 +988,34 @@ class StockEntry(StockController): & (se.docstatus == 1) & (se_detail.item_code == se_item.item_code) & ( - (se.purchase_order == self.purchase_order) + ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail)) if self.subcontract_data.order_doctype == "Purchase Order" - else (se.subcontracting_order == self.subcontracting_order) + else ( + (se.subcontracting_order == self.subcontracting_order) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + ) ) ) - ).run()[0][0] + ).run()[0][0] or 0 - if flt(total_supplied, precision) > flt(total_allowed, precision): + total_returned = 0 + if self.subcontract_data.order_doctype == "Subcontracting Order": + total_returned = ( + frappe.qb.from_(se) + .inner_join(se_detail) + .on(se.name == se_detail.parent) + .select(Sum(se_detail.transfer_qty)) + .where( + (se.purpose == "Material Transfer") + & (se.docstatus == 1) + & (se.is_return == 1) + & (se_detail.item_code == se_item.item_code) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + & (se.subcontracting_order == self.subcontracting_order) + ) + ).run()[0][0] or 0 + + if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision): frappe.throw( _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format( se_item.idx, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index de74fda687d..c4f26c4baaf 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -447,9 +447,7 @@ class TestStockEntry(FrappeTestCase): repack.posting_date = nowdate() repack.posting_time = nowtime() - expenses_included_in_valuation = frappe.get_value( - "Company", company, "expenses_included_in_valuation" - ) + default_expense_account = frappe.get_value("Company", company, "default_expense_account") items = get_multiple_items() repack.items = [] @@ -460,12 +458,12 @@ class TestStockEntry(FrappeTestCase): "additional_costs", [ { - "expense_account": expenses_included_in_valuation, + "expense_account": default_expense_account, "description": "Actual Operating Cost", "amount": 1000, }, { - "expense_account": expenses_included_in_valuation, + "expense_account": default_expense_account, "description": "Additional Operating Cost", "amount": 200, }, @@ -504,9 +502,7 @@ class TestStockEntry(FrappeTestCase): self.check_gl_entries( "Stock Entry", repack.name, - sorted( - [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]] - ), + sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]), ) def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): @@ -1508,6 +1504,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].item_name, item.item_name) self.assertEqual(se.items[0].stock_uom, item.stock_uom) + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_reposting_for_depedent_warehouse(self): from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 46ce9debf3b..b07c0876a25 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -11,6 +11,7 @@ "warehouse", "posting_date", "posting_time", + "is_adjustment_entry", "column_break_6", "voucher_type", "voucher_no", @@ -309,6 +310,12 @@ "label": "Recalculate Incoming/Outgoing Rate", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_adjustment_entry", + "fieldtype": "Check", + "label": "Is Adjustment Entry" } ], "hide_toolbar": 1, @@ -317,7 +324,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-21 06:25:30.040801", + "modified": "2023-10-23 18:07:42.063615", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", @@ -341,4 +348,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 05dd105d99d..1415aa36164 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -95,13 +95,6 @@ frappe.ui.form.on("Stock Reconciliation", { fieldname: "item_code", fieldtype: "Link", options: "Item", - "get_query": function() { - return { - "filters": { - "disabled": 0, - } - }; - } }, { label: __("Ignore Empty Stock"), diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index e469291eac9..dd39f103cd7 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -269,6 +269,10 @@ class StockReconciliation(StockController): if item.has_batch_no: has_batch_no = True + if not row.qty and not row.valuation_rate and not row.current_qty: + self.make_adjustment_entry(row, sl_entries) + continue + if item.has_serial_no or item.has_batch_no: has_serial_no = True self.get_sle_for_serialized_items(row, sl_entries, item) @@ -402,6 +406,21 @@ class StockReconciliation(StockController): # update valuation rate self.update_valuation_rate_for_serial_nos(row, serial_nos) + def make_adjustment_entry(self, row, sl_entries): + from erpnext.stock.stock_ledger import get_stock_value_difference + + difference_amount = get_stock_value_difference( + row.item_code, row.warehouse, self.posting_date, self.posting_time + ) + + if not difference_amount: + return + + args = self.get_sle_for_items(row) + args.update({"stock_value_difference": -1 * difference_amount, "is_adjustment_entry": 1}) + + sl_entries.append(args) + def update_valuation_rate_for_serial_no(self): for d in self.items: if not d.serial_no: @@ -647,7 +666,7 @@ class StockReconciliation(StockController): {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, "name", ) - and current_qty + and current_qty > 0 ): new_sle = self.get_sle_for_items(row) new_sle.actual_qty = current_qty * -1 @@ -890,6 +909,7 @@ def get_stock_balance_for( with_valuation_rate=with_valuation_rate, with_serial_no=has_serial_no, inventory_dimensions_dict=inventory_dimensions_dict, + batch_no=batch_no, ) if has_serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index c913af3301a..05c60175f51 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -702,6 +702,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sl_entry.actual_qty), 1.0) self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0) + @change_settings("Stock Reposting Settings", {"item_based_reposting": 0}) def test_backdated_stock_reco_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -1008,6 +1009,52 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(sr2.docstatus, 1) + def test_current_qty_and_current_serial_no_count(self): + # Step - 1: Create a Serial Batch Item + item = self.make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TEST-SERIAL-.###", + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.###", + } + ).name + + # Step - 2: Inward stock in multiple Batches + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se1 = make_stock_entry( + item_code=item, + target="_Test Warehouse - _TC", + qty=10, + ) + se2 = make_stock_entry( + item_code=item, + target="_Test Warehouse - _TC", + qty=5, + ) + + # Step - 3: Create Stock Reconciliation + sr = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=0, + batch_no=se1.items[0].batch_no, + do_not_submit=True, + ) + + # Test - 1: Current Serial No Count should be equal to Current Qty + self.assertEqual(sr.items[0].current_qty, se1.items[0].qty) + self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty) + + sr.items[0].batch_no = se2.items[0].batch_no + sr.save() + + self.assertEqual(sr.items[0].current_qty, se2.items[0].qty) + self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 7c712ce2258..68afd996b49 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -50,7 +50,7 @@ "label": "Limit timeslot for Stock Reposting" }, { - "default": "0", + "default": "1", "fieldname": "item_based_reposting", "fieldtype": "Check", "label": "Use Item based reposting" @@ -70,7 +70,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-04 16:14:29.080697", + "modified": "2023-11-01 16:14:29.080697", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 92c945b254b..990e03daa1b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -302,7 +302,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of: + if item.variant_of and not item.taxes: item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) @@ -364,8 +364,12 @@ def get_basic_details(args, item, overwrite_warehouse=True): ), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), - "discount_account": get_default_discount_account(args, item_defaults), - "provisional_expense_account": get_provisional_account(args, item_defaults), + "discount_account": get_default_discount_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "provisional_expense_account": get_provisional_account( + args, item_defaults, item_group_defaults, brand_defaults + ), "cost_center": get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ), @@ -719,12 +723,22 @@ def get_default_expense_account(args, item, item_group, brand): ) -def get_provisional_account(args, item): - return item.get("default_provisional_account") or args.default_provisional_account +def get_provisional_account(args, item, item_group, brand): + return ( + item.get("default_provisional_account") + or item_group.get("default_provisional_account") + or brand.get("default_provisional_account") + or args.default_provisional_account + ) -def get_default_discount_account(args, item): - return item.get("default_discount_account") or args.discount_account +def get_default_discount_account(args, item, item_group, brand): + return ( + item.get("default_discount_account") + or item_group.get("default_discount_account") + or brand.get("default_discount_account") + or args.discount_account + ) def get_default_deferred_account(args, item, fieldname=None): @@ -771,6 +785,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan data = frappe.get_attr(path)(args.get("item_code"), company) if data and (data.selling_cost_center or data.buying_cost_center): + if args.get("customer") and data.selling_cost_center: + return data.selling_cost_center + + elif args.get("supplier") and data.buying_cost_center: + return data.buying_cost_center + return data.selling_cost_center or data.buying_cost_center if not cost_center and args.get("cost_center"): diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index f00dd3e7912..90b8d453420 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -11,6 +11,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Warehouse'), fieldtype:'Link', options:'Warehouse', + default: frappe.route_options && frappe.route_options.warehouse, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); @@ -22,6 +23,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Item'), fieldtype:'Link', options:'Item', + default: frappe.route_options && frappe.route_options.item_code, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); @@ -33,6 +35,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { label: __('Item Group'), fieldtype:'Link', options:'Item Group', + default: frappe.route_options && frappe.route_options.item_group, change: function() { page.item_dashboard.start = 0; page.item_dashboard.refresh(); diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index bdae87c7ee7..8a262497926 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -635,9 +635,11 @@ class update_entries_after(object): sle.valuation_rate = self.wh_data.valuation_rate sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) - sle.stock_value_difference = stock_value_difference - sle.doctype = "Stock Ledger Entry" + if not sle.is_adjustment_entry or not self.args.get("sle_id"): + sle.stock_value_difference = stock_value_difference + + sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() if not self.args.get("sle_id"): @@ -1711,3 +1713,27 @@ def is_internal_transfer(sle): if data.is_internal_supplier and data.represents_company == data.company: return True + + +def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None): + table = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(table) + .select(Sum(table.stock_value_difference).as_("value")) + .where( + (table.is_cancelled == 0) + & (table.item_code == item_code) + & (table.warehouse == warehouse) + & ( + (table.posting_date < posting_date) + | ((table.posting_date == posting_date) & (table.posting_time <= posting_time)) + ) + ) + ) + + if voucher_no: + query = query.where(table.voucher_no != voucher_no) + + difference_amount = query.run() + return flt(difference_amount[0][0]) if difference_amount else 0 diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9f654fc6632..e019c572daf 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -95,6 +95,7 @@ def get_stock_balance( with_valuation_rate=False, with_serial_no=False, inventory_dimensions_dict=None, + batch_no=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -124,6 +125,9 @@ def get_stock_balance( if with_valuation_rate: if with_serial_no: + if batch_no: + args["batch_no"] = batch_no + serial_nos = get_serial_nos_data_after_transactions(args) return ( @@ -140,27 +144,30 @@ def get_stock_balance( def get_serial_nos_data_after_transactions(args): - serial_nos = set() args = frappe._dict(args) - sle = frappe.qb.DocType("Stock Ledger Entry") - stock_ledger_entries = ( + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( frappe.qb.from_(sle) - .select("serial_no", "actual_qty") + .select(sle.serial_no, sle.actual_qty) .where( - (sle.item_code == args.item_code) + (sle.is_cancelled == 0) + & (sle.item_code == args.item_code) & (sle.warehouse == args.warehouse) & ( CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(args.posting_date, args.posting_time) ) - & (sle.is_cancelled == 0) ) .orderby(sle.posting_date, sle.posting_time, sle.creation) - .run(as_dict=1) ) + if args.batch_no: + query = query.where(sle.batch_no == args.batch_no) + + stock_ledger_entries = query.run(as_dict=True) + for stock_ledger_entry in stock_ledger_entries: changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) if stock_ledger_entry.actual_qty > 0: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 15a2ac90912..d9c503b3de8 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', { get_materials_from_supplier: function (frm) { let sco_rm_details = []; - if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { + if (frm.doc.status != "Closed" && frm.doc.supplied_items) { frm.doc.supplied_items.forEach(d => { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { sco_rm_details.push(d.name); @@ -193,7 +193,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } has_unsupplied_items() { - return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + return this.frm.doc['supplied_items'].some(item => item.required_qty > (item.supplied_qty - item.returned_qty)); } make_subcontracting_receipt() { diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index d77e77440e0..46c229bfd37 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -112,6 +112,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -337,7 +338,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-01-20 23:25:45.363281", + "modified": "2023-11-14 18:38:37.640677", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 130f38fb809..a83068f73fd 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -299,7 +299,6 @@ class SubcontractingReceipt(SubcontractingController): def make_item_gl_entries(self, gl_entries, warehouse_account=None): stock_rbnb = self.get_company_default("stock_received_but_not_billed") - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") warehouse_with_no_account = [] @@ -371,10 +370,7 @@ class SubcontractingReceipt(SubcontractingController): divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) if divisional_loss: - if self.is_return: - loss_account = expenses_included_in_valuation - else: - loss_account = item.expense_account + loss_account = item.expense_account self.add_gl_entry( gl_entries=gl_entries, diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index f7e88d0fb7f..28bd84e5895 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -104,6 +104,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -469,7 +470,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-09-03 17:04:21.214316", + "modified": "2023-11-14 18:38:26.459669", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index d4daacd4ea4..f96823b2908 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,13 +1,6 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frm.set_query("customer", function () { - return { - filters: { - "disabled": 0 - } - }; - }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html index d95b28961c6..8799a3b1eab 100644 --- a/erpnext/templates/includes/order/order_macros.html +++ b/erpnext/templates/includes/order/order_macros.html @@ -7,7 +7,7 @@ {% if d.thumbnail or d.image %} {{ product_image(d.thumbnail or d.image, no_border=True) }} {% else %} -
{{ d.description }}
+{{ d.description }}
{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}{% if supplier_part_no %} {{_("Supplier Part No") + ": "+ supplier_part_no}} {% endif %}
-