diff --git a/README.md b/README.md index 44bd729688..710187ad2f 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,6 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB 1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines) 1. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) -1. [Translations](https://translate.erpnext.com) - ## License diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 061bab320e..fd052d0476 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -66,7 +66,12 @@ "show_balance_in_coa", "banking_tab", "enable_party_matching", - "enable_fuzzy_matching" + "enable_fuzzy_matching", + "reports_tab", + "remarks_section", + "general_ledger_remarks_length", + "column_break_lvjk", + "receivable_payable_remarks_length" ], "fields": [ { @@ -422,6 +427,34 @@ "fieldname": "round_row_wise_tax", "fieldtype": "Check", "label": "Round Tax Amount Row-wise" + }, + { + "fieldname": "reports_tab", + "fieldtype": "Tab Break", + "label": "Reports" + }, + { + "default": "0", + "description": "Truncates 'Remarks' column to set character length", + "fieldname": "general_ledger_remarks_length", + "fieldtype": "Int", + "label": "General Ledger" + }, + { + "default": "0", + "description": "Truncates 'Remarks' column to set character length", + "fieldname": "receivable_payable_remarks_length", + "fieldtype": "Int", + "label": "Accounts Receivable/Payable" + }, + { + "fieldname": "column_break_lvjk", + "fieldtype": "Column Break" + }, + { + "fieldname": "remarks_section", + "fieldtype": "Section Break", + "label": "Remarks Column Length" } ], "icon": "icon-cog", @@ -429,7 +462,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-28 00:12:02.740633", + "modified": "2023-11-20 09:37:47.650347", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", 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 d61f8a6c01..56fa6ce2f3 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 d6e1be4123..5a1c139bde 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 0000000000..85a2f2112f --- /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/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 22b6880ad5..9684a0d9d1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Journal Entry", { }, __('Make')); } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); }, before_save: function(frm) { if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 2eb54a54d5..906760ec31 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -548,8 +548,16 @@ "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, - "links": [], - "modified": "2023-08-10 14:32:22.366895", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Bank Transaction Payments", + "link_fieldname": "payment_entry", + "parent_doctype": "Bank Transaction", + "table_fieldname": "payment_entries" + } + ], + "modified": "2023-11-23 12:11:04.128015", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index c70ad2fdaa..0b5a37f206 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -910,7 +910,7 @@ class JournalEntry(AccountsController): party_account_currency = d.account_currency elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]: - bank_amount += d.debit_in_account_currency or d.credit_in_account_currency + bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency) bank_account_currency = d.account_currency if party_type and pay_to_recd_from: diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 8d8c83751b..2b423ac51d 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -205,7 +205,8 @@ "fieldtype": "Select", "label": "Reference Type", "no_copy": 1, - "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry" + "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry", + "search_index": 1 }, { "fieldname": "reference_name", @@ -213,7 +214,8 @@ "in_list_view": 1, "label": "Reference Name", "no_copy": 1, - "options": "reference_type" + "options": "reference_type", + "search_index": 1 }, { "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", @@ -301,7 +303,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-08 12:20:21.489496", + "modified": "2023-11-23 11:44:25.841187", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 0203c45058..26112409b7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); @@ -154,13 +154,13 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); - if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) { + if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) { frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() { frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name}); }, __('Actions')); } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); }, validate_company: (frm) => { @@ -853,6 +853,7 @@ frappe.ui.form.on('Payment Entry', { var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { + total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount")) if(paid_amount > total_negative_outstanding) { if(total_negative_outstanding == 0) { frappe.msgprint( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index d7b6a198df..aa181564b0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -595,6 +595,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", + "no_copy": 1, "options": "\nDraft\nSubmitted\nCancelled", "read_only": 1 }, @@ -749,8 +750,16 @@ ], "index_web_pages_for_search": 1, "is_submittable": 1, - "links": [], - "modified": "2023-06-23 18:07:38.023010", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Bank Transaction Payments", + "link_fieldname": "payment_entry", + "parent_doctype": "Bank Transaction", + "table_fieldname": "payment_entries" + } + ], + "modified": "2023-11-23 12:07:20.887885", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ff8695f770..16b32bd4ab 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -33,6 +33,7 @@ from erpnext.accounts.utils import ( get_account_currency, get_balance_on, get_outstanding_invoices, + get_party_types_from_account_type, ) from erpnext.controllers.accounts_controller import ( AccountsController, @@ -83,7 +84,6 @@ class PaymentEntry(AccountsController): self.apply_taxes() self.set_amounts_after_tax() self.clear_unallocated_reference_document_rows() - self.validate_payment_against_negative_invoice() self.validate_transaction_reference() self.set_title() self.set_remarks() @@ -148,7 +148,7 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", - "Unreconcile Payments", + "Unreconcile Payment", "Unreconcile Payment Entries", ) super(PaymentEntry, self).on_cancel() @@ -952,35 +952,6 @@ class PaymentEntry(AccountsController): self.name, ) - def validate_payment_against_negative_invoice(self): - if (self.payment_type != "Pay" or self.party_type != "Customer") and ( - self.payment_type != "Receive" or self.party_type != "Supplier" - ): - return - - total_negative_outstanding = sum( - abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 - ) - - paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount - additional_charges = sum(flt(d.amount) for d in self.deductions) - - if not total_negative_outstanding: - if self.party_type == "Customer": - msg = _("Cannot pay to Customer without any negative outstanding invoice") - else: - msg = _("Cannot receive from Supplier without any negative outstanding invoice") - - frappe.throw(msg, InvalidPaymentEntry) - - elif paid_amount - additional_charges > total_negative_outstanding: - frappe.throw( - _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( - fmt_money(total_negative_outstanding) - ), - InvalidPaymentEntry, - ) - def set_title(self): if frappe.flags.in_import and self.title: # do not set title dynamically if title exists during data import. @@ -1051,6 +1022,7 @@ class PaymentEntry(AccountsController): self.add_bank_gl_entries(gl_entries) self.add_deductions_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries) + add_regional_gl_entries(gl_entries, self) return gl_entries def make_gl_entries(self, cancel=0, adv_adj=0): @@ -1085,11 +1057,9 @@ class PaymentEntry(AccountsController): item=self, ) - dr_or_cr = ( - "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit" - ) - for d in self.get("references"): + # re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse + dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" cost_center = self.cost_center if d.reference_doctype == "Sales Invoice" and not cost_center: cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") @@ -1105,10 +1075,25 @@ class PaymentEntry(AccountsController): against_voucher_type = d.reference_doctype against_voucher = d.reference_name + reverse_dr_or_cr = 0 + if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]: + is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return") + payable_party_types = get_party_types_from_account_type("Payable") + receivable_party_types = get_party_types_from_account_type("Receivable") + if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"): + reverse_dr_or_cr = 1 + elif ( + is_return and self.party_type in payable_party_types and (self.payment_type == "Receive") + ): + reverse_dr_or_cr = 1 + + if is_return and not reverse_dr_or_cr: + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + gle.update( { - dr_or_cr: allocated_amount_in_company_currency, - dr_or_cr + "_in_account_currency": d.allocated_amount, + dr_or_cr: abs(allocated_amount_in_company_currency), + dr_or_cr + "_in_account_currency": abs(d.allocated_amount), "against_voucher_type": against_voucher_type, "against_voucher": against_voucher, "cost_center": cost_center, @@ -1116,6 +1101,7 @@ class PaymentEntry(AccountsController): ) gl_entries.append(gle) + dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" if self.unallocated_amount: exchange_rate = self.get_exchange_rate() base_unallocated_amount = self.unallocated_amount * exchange_rate @@ -1711,13 +1697,42 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company): - invoice_ref_based_on_payment_terms = {} +def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: + """Split a list of invoices based on their payment terms.""" + exc_rates = get_currency_data(outstanding_invoices, company) + outstanding_invoices_after_split = [] + for entry in outstanding_invoices: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + if payment_term_template := frappe.db.get_value( + entry.voucher_type, entry.voucher_no, "payment_terms_template" + ): + split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates) + if not split_rows: + continue + + frappe.msgprint( + _("Splitting {0} {1} into {2} rows as per Payment Terms").format( + _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) + ), + alert=True, + ) + outstanding_invoices_after_split += split_rows + continue + + # If not an invoice or no payment terms template, add as it is + outstanding_invoices_after_split.append(entry) + + return outstanding_invoices_after_split + + +def get_currency_data(outstanding_invoices: list, company: str = None) -> dict: + """Get currency and conversion data for a list of invoices.""" + exc_rates = frappe._dict() company_currency = ( frappe.db.get_value("Company", company, "default_currency") if company else None ) - exc_rates = frappe._dict() + for doctype in ["Sales Invoice", "Purchase Invoice"]: invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] for x in frappe.db.get_all( @@ -1732,72 +1747,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): company_currency=company_currency, ) - for idx, d in enumerate(outstanding_invoices): - if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: - payment_term_template = frappe.db.get_value( - d.voucher_type, d.voucher_no, "payment_terms_template" + return exc_rates + + +def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list: + """Split invoice based on its payment schedule table.""" + split_rows = [] + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" + ) + + if not allocate_payment_based_on_payment_terms: + return [invoice] + + payment_schedule = frappe.get_all( + "Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date" + ) + for payment_term in payment_schedule: + if not payment_term.outstanding > 0.1: + continue + + doc_details = exc_rates.get(payment_term.parent, None) + is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( + doc_details.party_account_currency != doc_details.company_currency + ) + payment_term_outstanding = flt(payment_term.outstanding) + if not is_multi_currency_acc: + payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) + + split_rows.append( + frappe._dict( + { + "due_date": invoice.due_date, + "currency": invoice.currency, + "voucher_no": invoice.voucher_no, + "voucher_type": invoice.voucher_type, + "posting_date": invoice.posting_date, + "invoice_amount": flt(invoice.invoice_amount), + "outstanding_amount": payment_term_outstanding + if payment_term_outstanding + else invoice.outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, + "payment_amount": payment_term.payment_amount, + "payment_term": payment_term.payment_term, + } ) - if payment_term_template: - allocate_payment_based_on_payment_terms = frappe.get_cached_value( - "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" - ) - if allocate_payment_based_on_payment_terms: - payment_schedule = frappe.get_all( - "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"] - ) + ) - for payment_term in payment_schedule: - if payment_term.outstanding > 0.1: - doc_details = exc_rates.get(payment_term.parent, None) - is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( - doc_details.party_account_currency != doc_details.company_currency - ) - payment_term_outstanding = flt(payment_term.outstanding) - if not is_multi_currency_acc: - payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) - - invoice_ref_based_on_payment_terms.setdefault(idx, []) - invoice_ref_based_on_payment_terms[idx].append( - frappe._dict( - { - "due_date": d.due_date, - "currency": d.currency, - "voucher_no": d.voucher_no, - "voucher_type": d.voucher_type, - "posting_date": d.posting_date, - "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": payment_term_outstanding - if payment_term_outstanding - else d.outstanding_amount, - "payment_term_outstanding": payment_term_outstanding, - "payment_amount": payment_term.payment_amount, - "payment_term": payment_term.payment_term, - "account": d.account, - } - ) - ) - - outstanding_invoices_after_split = [] - if invoice_ref_based_on_payment_terms: - for idx, ref in invoice_ref_based_on_payment_terms.items(): - voucher_no = ref[0]["voucher_no"] - voucher_type = ref[0]["voucher_type"] - - frappe.msgprint( - _("Spliting {} {} into {} row(s) as per Payment Terms").format( - voucher_type, voucher_no, len(ref) - ), - alert=True, - ) - - outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx] - - existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices)) - index = outstanding_invoices.index(existing_row[0]) - outstanding_invoices.pop(index) - - outstanding_invoices_after_split += outstanding_invoices - return outstanding_invoices_after_split + return split_rows def get_orders_to_be_billed( @@ -2638,3 +2635,8 @@ def make_payment_order(source_name, target_doc=None): ) return doclist + + +@erpnext.allow_regional +def add_regional_gl_entries(gl_entries, doc): + return diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index edfec41918..f4b0c55313 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -6,10 +6,11 @@ import unittest import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import flt, nowdate +from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( InvalidPaymentEntry, + get_outstanding_reference_documents, get_payment_entry, get_reference_details, ) @@ -683,17 +684,6 @@ class TestPaymentEntry(FrappeTestCase): self.validate_gl_entries(pe.name, expected_gle) def test_payment_against_negative_sales_invoice(self): - pe1 = frappe.new_doc("Payment Entry") - pe1.payment_type = "Pay" - pe1.company = "_Test Company" - pe1.party_type = "Customer" - pe1.party = "_Test Customer" - pe1.paid_from = "_Test Cash - _TC" - pe1.paid_amount = 100 - pe1.received_amount = 100 - - self.assertRaises(InvalidPaymentEntry, pe1.validate) - si1 = create_sales_invoice() # create full payment entry against si1 @@ -751,8 +741,6 @@ class TestPaymentEntry(FrappeTestCase): # pay more than outstanding against si1 pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") - pe3.paid_amount = pe3.received_amount = 300 - self.assertRaises(InvalidPaymentEntry, pe3.validate) # pay negative outstanding against si1 pe3.paid_to = "Debtors - _TC" @@ -1262,6 +1250,130 @@ class TestPaymentEntry(FrappeTestCase): so.reload() self.assertEqual(so.advance_paid, so.rounded_total) + def test_outstanding_invoices_api(self): + """ + Test if `get_outstanding_reference_documents` fetches invoices in the right order. + """ + customer = create_customer("Max Mustermann", "INR") + create_payment_terms_template() + + # SI has an earlier due date and SI2 has a later due date + si = create_sales_invoice( + qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4) + ) + si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer) + si2.payment_terms_template = "Test Receivable Template" + si2.submit() + + args = { + "posting_date": nowdate(), + "company": "_Test Company", + "party_type": "Customer", + "payment_type": "Pay", + "party": customer, + "party_account": "Debtors - _TC", + } + args.update( + { + "get_outstanding_invoices": True, + "from_posting_date": add_days(nowdate(), -4), + "to_posting_date": add_days(nowdate(), 2), + } + ) + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 3) + self.assertEqual(references[0].voucher_no, si.name) + self.assertEqual(references[1].voucher_no, si2.name) + self.assertEqual(references[2].voucher_no, si2.name) + self.assertEqual(references[1].payment_term, "Basic Amount Receivable") + self.assertEqual(references[2].payment_term, "Tax Receivable") + + def test_receive_payment_from_payable_party_type(self): + """ + Checks GL entries generated while receiving payments from a Payable Party Type. + """ + pe = create_payment_entry( + party_type="Supplier", + party="_Test Supplier", + payment_type="Receive", + paid_from="Creditors - _TC", + paid_to="_Test Cash - _TC", + save=True, + submit=True, + ) + self.voucher_no = pe.name + self.expected_gle = [ + {"account": "Creditors - _TC", "debit": 0.0, "credit": 1000.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + self.check_gl_entries() + + def test_payment_against_partial_return_invoice(self): + """ + Checks GL entries generated for partial return invoice payments. + """ + si = create_sales_invoice(qty=10, rate=10, customer="_Test Customer") + credit_note = create_sales_invoice( + qty=-4, rate=10, customer="_Test Customer", is_return=1, return_against=si.name + ) + pe = create_payment_entry( + party_type="Customer", + party="_Test Customer", + payment_type="Receive", + paid_from="Debtors - _TC", + paid_to="_Test Cash - _TC", + ) + pe.set( + "references", + [ + { + "reference_doctype": "Sales Invoice", + "reference_name": si.name, + "due_date": si.get("due_date"), + "total_amount": si.grand_total, + "outstanding_amount": si.outstanding_amount, + "allocated_amount": si.outstanding_amount, + }, + { + "reference_doctype": "Sales Invoice", + "reference_name": credit_note.name, + "due_date": credit_note.get("due_date"), + "total_amount": credit_note.grand_total, + "outstanding_amount": credit_note.outstanding_amount, + "allocated_amount": credit_note.outstanding_amount, + }, + ], + ) + pe.save() + pe.submit() + self.assertEqual(pe.total_allocated_amount, 60) + self.assertEqual(pe.unallocated_amount, 940) + self.voucher_no = pe.name + self.expected_gle = [ + {"account": "Debtors - _TC", "debit": 40.0, "credit": 0.0}, + {"account": "Debtors - _TC", "debit": 0.0, "credit": 940.0}, + {"account": "Debtors - _TC", "debit": 0.0, "credit": 100.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + self.check_gl_entries() + + def check_gl_entries(self): + gle = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + ) + .where((gle.voucher_no == self.voucher_no) & (gle.is_cancelled == 0)) + .orderby(gle.account, gle.debit, gle.credit, order=frappe.qb.desc) + ).run(as_dict=True) + for row in range(len(self.expected_gle)): + for field in ["account", "debit", "credit"]: + self.assertEqual(self.expected_gle[row][field], gl_entries[row][field]) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") @@ -1322,6 +1434,9 @@ def create_payment_terms_template(): def create_payment_terms_template_with_discount( name=None, discount_type=None, discount=None, template_name=None ): + """ + Create a Payment Terms Template with % or amount discount. + """ create_payment_term(name or "30 Credit Days with 10% Discount") template_name = template_name or "Test Discount Template" diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index b88791d3f9..ccb9e648cb 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -212,9 +212,10 @@ ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", + "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-08-15 05:35:50.109290", + "modified": "2023-11-17 17:33:55.701726", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -239,6 +240,5 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [], - "track_changes": 1 + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 43167be15a..6673e8de28 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,58 @@ class PaymentReconciliation(Document): self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + def load_from_db(self): + # 'modified' attribute is required for `run_doc_method` to work properly. + doc_dict = frappe._dict( + { + "modified": None, + "company": None, + "party": None, + "party_type": None, + "receivable_payable_account": None, + "default_advance_account": None, + "from_invoice_date": None, + "to_invoice_date": None, + "invoice_limit": 50, + "from_payment_date": None, + "to_payment_date": None, + "payment_limit": 50, + "minimum_invoice_amount": None, + "minimum_payment_amount": None, + "maximum_invoice_amount": None, + "maximum_payment_amount": None, + "bank_cash_account": None, + "cost_center": None, + "payment_name": None, + "invoice_name": None, + } + ) + super(Document, self).__init__(doc_dict) + + def save(self): + return + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + @frappe.whitelist() def get_unreconciled_entries(self): self.get_nonreconciled_payment_entries() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 71bc498b49..d7a73f0ce7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1137,6 +1137,40 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pay.unallocated_amount, 1000) self.assertEqual(pay.difference_amount, 0) + def test_rounding_of_unallocated_amount(self): + self.supplier = "_Test Supplier USD" + pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True) + pi.supplier = self.supplier + pi.currency = "USD" + pi.conversion_rate = 80 + pi.credit_to = self.creditors_usd + pi.save().submit() + + pe = get_payment_entry(pi.doctype, pi.name) + pe.target_exchange_rate = 78.726500000 + pe.received_amount = 26.75 + pe.paid_amount = 2105.93 + pe.references = [] + pe.save().submit() + + # unallocated_amount will have some rounding loss - 26.749950 + self.assertNotEqual(pe.unallocated_amount, 26.75) + + 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 = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again. + pr.reconcile() + 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 5b8556e7c8..491c67818d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -159,9 +159,10 @@ "label": "Difference Posting Date" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-10-23 10:44:56.066303", + "modified": "2023-11-17 17:33:38.612615", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index c4dbd7e844..7c9d49e773 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -71,9 +71,10 @@ "label": "Exchange Rate" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2022-11-08 18:18:02.502149", + "modified": "2023-11-17 17:33:45.455166", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 17f3900880..d199236ae9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -107,9 +107,10 @@ "options": "Cost Center" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-09-03 07:43:29.965353", + "modified": "2023-11-17 17:33:34.818530", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 5f0b434c70..c2e01c4ba3 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -175,13 +175,6 @@ class PaymentRequest(Document): if self.payment_url: self.db_set("payment_url", self.payment_url) - if ( - self.payment_url - or not self.payment_gateway_account - or (self.payment_gateway_account and self.payment_channel == "Phone") - ): - self.db_set("status", "Initiated") - def get_payment_url(self): if self.reference_doctype != "Fees": data = frappe.db.get_value( diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index f6047079ff..955b66a1b8 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -18,6 +18,7 @@ "is_pos", "is_return", "update_billed_amount_in_sales_order", + "update_billed_amount_in_delivery_note", "column_break1", "company", "posting_date", @@ -1550,12 +1551,19 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "default": "1", + "depends_on": "eval: doc.is_return && doc.return_against", + "fieldname": "update_billed_amount_in_delivery_note", + "fieldtype": "Check", + "label": "Update Billed Amount in Delivery Note" } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:23:41.083409", + "modified": "2023-11-20 12:27:12.848149", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e36e97bc4b..9091a77f99 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -556,7 +556,7 @@ def get_stock_availability(item_code, warehouse): return bin_qty - pos_sales_qty, is_stock_item else: is_stock_item = True - if frappe.db.exists("Product Bundle", item_code): + if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): return get_bundle_availability(item_code, warehouse), is_stock_item else: is_stock_item = False diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 982bdc198a..200b82a447 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -6,7 +6,6 @@ import unittest import frappe from frappe import _ -from frappe.utils import add_days, nowdate from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile @@ -126,64 +125,70 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(inv.grand_total, 5474.0) def test_tax_calculation_with_item_tax_template(self): - import json + inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1) + item_row = inv.get("items")[0] - from erpnext.stock.get_item_details import get_item_details - - # set tax template in item - item = frappe.get_cached_doc("Item", "_Test Item") - item.set( - "taxes", - [ - { - "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", - "valid_from": add_days(nowdate(), -5), - } - ], - ) - item.save() - - # create POS invoice with item - pos_inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True) - item_details = get_item_details( - doc=pos_inv, - args={ - "item_code": item.item_code, - "company": pos_inv.company, - "doctype": "POS Invoice", - "conversion_rate": 1.0, - }, - ) - tax_map = json.loads(item_details.item_tax_rate) - for tax in tax_map: - pos_inv.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": tax, - "rate": tax_map[tax], - "description": "Test", - "cost_center": "_Test Cost Center - _TC", - }, - ) - pos_inv.submit() - pos_inv.load_from_db() - - # check if correct tax values are applied from tax template - self.assertEqual(pos_inv.net_total, 386.4) - - expected_taxes = [ - { - "tax_amount": 57.96, - "total": 444.36, - }, + add_items = [ + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), ] + for qty, item_tax_template in add_items: + item_row_copy = copy.deepcopy(item_row) + item_row_copy.qty = qty + item_row_copy.item_tax_template = item_tax_template + inv.append("items", item_row_copy) - for i in range(len(expected_taxes)): - for key in expected_taxes[i]: - self.assertEqual(expected_taxes[i][key], pos_inv.get("taxes")[i].get(key)) + inv.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, + }, + ) + inv.insert() - self.assertEqual(pos_inv.get("base_total_taxes_and_charges"), 57.96) + self.assertEqual(inv.net_total, 4600) + + self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41) + self.assertEqual(inv.get("taxes")[0].total, 5102.41) + + self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80) + self.assertEqual(inv.get("taxes")[1].total, 5300.21) + + self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36) + self.assertEqual(inv.get("taxes")[2].total, 5675.57) + + self.assertEqual(inv.grand_total, 5675.57) + self.assertEqual(inv.rounding_adjustment, 0.43) + self.assertEqual(inv.rounded_total, 5676.0) def test_tax_calculation_with_multiple_items_and_discount(self): inv = create_pos_invoice(qty=1, rate=75, do_not_save=True) 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 cb0ed3d6aa..5a281aaa4f 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -186,6 +186,7 @@ "label": "Image" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -833,7 +834,7 @@ ], "istable": 1, "links": [], - "modified": "2023-03-12 13:36:40.160468", + "modified": "2023-11-14 18:33:22.585715", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 2eaa33767c..4b0df12f45 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -180,7 +180,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } unblock_invoice() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 53c131a507..e7d29727ed 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -13,6 +13,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, ) from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( check_if_return_invoice_linked_with_payment_entry, @@ -491,6 +492,7 @@ class PurchaseInvoice(BuyingController): def validate_for_repost(self): self.validate_write_off_account() self.validate_expense_account() + validate_docs_for_voucher_types(["Purchase Invoice"]) validate_docs_for_deferred_accounting([], [self.name]) def on_submit(self): @@ -525,7 +527,11 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - self.update_project() + if ( + frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" + ): + self.update_project() + update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.update_advance_tax_references() @@ -1302,7 +1308,10 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - self.update_project() + if ( + frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" + ): + self.update_project() self.db_set("status", "Cancelled") unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) @@ -1321,13 +1330,21 @@ class PurchaseInvoice(BuyingController): self.update_advance_tax_references(cancel=1) def update_project(self): - project_list = [] + projects = frappe._dict() for d in self.items: - if d.project and d.project not in project_list: - project = frappe.get_doc("Project", d.project) - project.update_purchase_costing() - project.db_update() - project_list.append(d.project) + if d.project: + if self.docstatus == 1: + projects[d.project] = projects.get(d.project, 0) + d.base_net_amount + elif self.docstatus == 2: + projects[d.project] = projects.get(d.project, 0) - d.base_net_amount + + pj = frappe.qb.DocType("Project") + for proj, value in projects.items(): + res = ( + frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run() + ) + current_purchase_cost = res and res[0][0] or 0 + frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value) def validate_supplier_invoice(self): if self.bill_date: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 13593bcf9b..171cc0ccdf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): set_advance_flag(company="_Test Company", flag=0, default_account="") def test_gl_entries_for_standalone_debit_note(self): - make_purchase_invoice(qty=5, rate=500, update_stock=True) + from erpnext.stock.doctype.item.test_item import make_item - returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True) + item_code = make_item(properties={"is_stock_item": 1}) + make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True) + + returned_inv = make_purchase_invoice( + item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True + ) # override the rate with valuation rate sle = frappe.get_all( @@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): )[0] rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) - self.assertAlmostEqual(returned_inv.items[0].rate, rate) + self.assertAlmostEqual(rate, 500) def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( @@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): disable_dimension() def test_repost_accounting_entries(self): + # update repost settings + settings = frappe.get_doc("Repost Accounting Ledger Settings") + if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]: + settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True}) + settings.save() + pi = make_purchase_invoice( rate=1000, price_list_rate=1000, diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 424e942990..71796c9918 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -158,6 +158,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -497,6 +498,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -504,6 +506,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "default": ":Company", "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "cost_center", @@ -915,7 +918,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 3a87a380d1..c7b7a148cf 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 dbb0971fde..1d72a46c12 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,7 @@ 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 = get_allowed_types_from_settings() def validate(self): self.validate_vouchers() @@ -53,15 +51,7 @@ class RepostAccountingLedger(Document): def validate_vouchers(self): if self.vouchers: - # Validate voucher types - voucher_types = set([x.voucher_type for x in self.vouchers]) - if disallowed_types := voucher_types.difference(self._allowed_types): - frappe.throw( - _("{0} types are not allowed. Only {1} are.").format( - frappe.bold(comma_and(list(disallowed_types))), - frappe.bold(comma_and(list(self._allowed_types))), - ) - ) + validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers]) def get_existing_ledger_entries(self): vouchers = [x.voucher_no for x in self.vouchers] @@ -157,7 +147,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() @@ -165,6 +155,15 @@ def start_repost(account_repost_doc=str) -> None: frappe.db.commit() +def get_allowed_types_from_settings(): + return [ + x.document_type + for x in frappe.db.get_all( + "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] + ) + ] + + def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): docs_with_deferred_revenue = frappe.db.get_all( "Sales Invoice Item", @@ -186,3 +185,37 @@ 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])) ) ) + + +def validate_docs_for_voucher_types(doc_voucher_types): + allowed_types = get_allowed_types_from_settings() + # Validate voucher types + voucher_types = set(doc_voucher_types) + if disallowed_types := voucher_types.difference(allowed_types): + message = "are" if len(disallowed_types) > 1 else "is" + frappe.throw( + _("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format( + frappe.bold(comma_and(list(disallowed_types))), + message, + frappe.bold( + frappe.utils.get_link_to_form( + "Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings" + ) + ), + ) + ) + + +@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 0e75dd2e3e..dda0ec778f 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/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py similarity index 100% rename from erpnext/accounts/doctype/unreconcile_payments/__init__.py rename to erpnext/accounts/doctype/repost_accounting_ledger_settings/__init__.py 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 0000000000..8c83ca5043 --- /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 0000000000..8aa0a840c7 --- /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 0000000000..2b8230df86 --- /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 0000000000..ec4e87ffc0 --- /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 0000000000..e69de29bb2 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 0000000000..ede12fbc18 --- /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 0000000000..0e4883b0c9 --- /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 d4d923902f..6763e446a5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -184,10 +184,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } - make_maintenance_schedule() { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule", @@ -563,15 +562,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) { @@ -666,6 +656,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 cd725b9862..f2094874e0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1615,7 +1615,8 @@ "hide_seconds": 1, "label": "Inter Company Invoice Reference", "options": "Purchase Invoice", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "customer_group", @@ -2156,7 +2157,7 @@ "label": "Use Company default Cost Center for Round off" }, { - "default": "0", + "default": "1", "depends_on": "eval: doc.is_return", "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", @@ -2173,7 +2174,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-11-03 14:39:38.012346", + "modified": "2023-11-23 16:56:29.679499", "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 87b40c09bd..cc81227917 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -17,6 +17,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( ) from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, ) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, @@ -172,6 +173,7 @@ class SalesInvoice(SellingController): self.validate_write_off_account() self.validate_account_for_change_amount() self.validate_income_account() + validate_docs_for_voucher_types(["Sales Invoice"]) validate_docs_for_deferred_accounting([self.name], []) def validate_fixed_asset(self): @@ -395,7 +397,7 @@ class SalesInvoice(SellingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", - "Unreconcile Payments", + "Unreconcile Payment", "Unreconcile Payment Entries", "Payment Ledger Entry", "Serial and Batch Bundle", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 21cc253959..017bfa9654 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -516,72 +516,70 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(si.grand_total, 5474.0) def test_tax_calculation_with_item_tax_template(self): - import json - - from erpnext.stock.get_item_details import get_item_details - - # set tax template in item - item = frappe.get_cached_doc("Item", "_Test Item") - item.set( - "taxes", - [ - { - "item_tax_template": "_Test Item Tax Template 1 - _TC", - "valid_from": add_days(nowdate(), -5), - } - ], - ) - item.save() - - # create sales invoice with item si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True) - item_details = get_item_details( - doc=si, - args={ - "item_code": item.item_code, - "company": si.company, - "doctype": "Sales Invoice", - "conversion_rate": 1.0, + item_row = si.get("items")[0] + + add_items = [ + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), + ] + for qty, item_tax_template in add_items: + item_row_copy = copy.deepcopy(item_row) + item_row_copy.qty = qty + item_row_copy.item_tax_template = item_tax_template + si.append("items", item_row_copy) + + si.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, }, ) - tax_map = json.loads(item_details.item_tax_rate) - for tax in tax_map: - si.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": tax, - "rate": tax_map[tax], - "description": "Test", - "cost_center": "_Test Cost Center - _TC", - }, - ) - si.submit() - si.load_from_db() - - # check if correct tax values are applied from tax template - self.assertEqual(si.net_total, 386.4) - - expected_taxes = [ + si.append( + "taxes", { - "tax_amount": 19.32, - "total": 405.72, + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, }, + ) + si.append( + "taxes", { - "tax_amount": 38.64, - "total": 444.36, + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, }, - { - "tax_amount": 57.96, - "total": 502.32, - }, - ] + ) + si.insert() - for i in range(len(expected_taxes)): - for key in expected_taxes[i]: - self.assertEqual(expected_taxes[i][key], si.get("taxes")[i].get(key)) + self.assertEqual(si.net_total, 4600) - self.assertEqual(si.get("base_total_taxes_and_charges"), 115.92) + self.assertEqual(si.get("taxes")[0].tax_amount, 502.41) + self.assertEqual(si.get("taxes")[0].total, 5102.41) + + self.assertEqual(si.get("taxes")[1].tax_amount, 197.80) + self.assertEqual(si.get("taxes")[1].total, 5300.21) + + self.assertEqual(si.get("taxes")[2].tax_amount, 375.36) + self.assertEqual(si.get("taxes")[2].total, 5675.57) + + self.assertEqual(si.grand_total, 5675.57) + self.assertEqual(si.rounding_adjustment, 0.43) + self.assertEqual(si.rounded_total, 5676.0) def test_tax_calculation_with_multiple_items_and_discount(self): si = create_sales_invoice(qty=1, rate=75, do_not_save=True) @@ -791,6 +789,28 @@ class TestSalesInvoice(FrappeTestCase): w = self.make() self.assertEqual(w.outstanding_amount, w.base_rounded_total) + def test_rounded_total_with_cash_discount(self): + si = frappe.copy_doc(test_records[2]) + + item = copy.deepcopy(si.get("items")[0]) + item.update( + { + "qty": 1, + "rate": 14960.66, + } + ) + + si.set("items", [item]) + si.set("taxes", []) + si.apply_discount_on = "Grand Total" + si.is_cash_or_non_trade_discount = 1 + si.discount_amount = 1 + si.insert() + + self.assertEqual(si.grand_total, 14959.66) + self.assertEqual(si.rounded_total, 14960) + self.assertEqual(si.rounding_adjustment, 0.34) + def test_payment(self): w = self.make() 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 5d2764b669..a403b14c54 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, @@ -901,7 +902,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-26 12:53:22.404057", + "modified": "2023-11-14 18:34:10.479329", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -911,4 +912,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payment/__init__.py b/erpnext/accounts/doctype/unreconcile_payment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py similarity index 97% rename from erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py rename to erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 78e04bff81..f404d9981a 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -10,7 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): +class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() @@ -73,7 +73,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe.doctype, "voucher_no": pe.name, @@ -138,7 +138,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe2.doctype, "voucher_no": pe2.name, @@ -196,7 +196,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe.doctype, "voucher_no": pe.name, @@ -281,7 +281,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe2.doctype, "voucher_no": pe2.name, diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js similarity index 94% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js index c522567637..70cefb13b5 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Unreconcile Payments", { +frappe.ui.form.on("Unreconcile Payment", { refresh(frm) { frm.set_query("voucher_type", function() { return { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json similarity index 95% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json index f29e61b6ef..f906dc6cec 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json @@ -21,7 +21,7 @@ "fieldtype": "Link", "label": "Amended From", "no_copy": 1, - "options": "Unreconcile Payments", + "options": "Unreconcile Payment", "print_hide": 1, "read_only": 1 }, @@ -61,7 +61,7 @@ "modified": "2023-08-28 17:42:50.261377", "modified_by": "Administrator", "module": "Accounts", - "name": "Unreconcile Payments", + "name": "Unreconcile Payment", "naming_rule": "Expression", "owner": "Administrator", "permissions": [ @@ -90,4 +90,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py similarity index 98% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 4f9fb50d46..77906a7833 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -15,7 +15,7 @@ from erpnext.accounts.utils import ( ) -class UnreconcilePayments(Document): +class UnreconcilePayment(Document): def validate(self): self.supported_types = ["Payment Entry", "Journal Entry"] if not self.voucher_type in self.supported_types: @@ -142,7 +142,7 @@ def create_unreconcile_doc_for_selection(selections=None): selections = frappe.json.loads(selections) # assuming each row is a unique voucher for row in selections: - unrecon = frappe.new_doc("Unreconcile Payments") + unrecon = frappe.new_doc("Unreconcile Payment") unrecon.company = row.get("company") unrecon.voucher_type = row.get("voucher_type") unrecon.voucher_no = row.get("voucher_no") diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 16e73ea52f..5c18e506f5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -31,7 +31,12 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.utilities.regional import temporary_flag -PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} +PURCHASE_TRANSACTION_TYPES = { + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", +} SALES_TRANSACTION_TYPES = { "Quotation", "Sales Order", @@ -231,7 +236,9 @@ def set_address_details( if shipping_address: party_details.update( shipping_address=shipping_address, - shipping_address_display=render_address(shipping_address), + shipping_address_display=render_address( + shipping_address, check_permissions=not ignore_permissions + ), **get_fetch_values(doctype, "shipping_address", shipping_address) ) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index eff705dafa..b608ebc395 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -144,6 +144,16 @@ frappe.query_reports["Accounts Payable"] = { "label": __("Show Future Payments"), "fieldtype": "Check", }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "fieldtype": "Check", + }, + { + "fieldname": "for_revaluation_journals", + "label": __("Revaluation Journals"), + "fieldtype": "Check", + }, { "fieldname": "ignore_accounts", "label": __("Group by Voucher"), diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 9f03d92cd5..b4cb25ff1b 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } data = execute(filters) 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 9e575e669d..0f206b1cf4 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 786aad601b..b4bc8870d3 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", @@ -173,12 +176,23 @@ frappe.query_reports["Accounts Receivable"] = { "label": __("Show Remarks"), "fieldtype": "Check", }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "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) { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py old mode 100755 new mode 100644 index f24a24e42e..0e62ad61cc --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -7,14 +7,14 @@ from collections import OrderedDict import frappe from frappe import _, qb, scrub from frappe.query_builder import Criterion -from frappe.query_builder.functions import Date, Sum +from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) -from erpnext.accounts.utils import get_currency_precision +from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type # This report gives a summary of all Outstanding Invoices considering the following @@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision # 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters # 7. For overpayment against an invoice with payment terms, there will be an additional row # 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated -# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party -# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" +# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency +# 10. This report is based on Payment Ledger Entries def execute(filters=None): @@ -72,9 +72,7 @@ class ReceivablePayableReport(object): self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" self.account_type = self.filters.account_type - self.party_type = frappe.db.get_all( - "Party Type", {"account_type": self.account_type}, pluck="name" - ) + self.party_type = get_party_types_from_account_type(self.account_type) self.party_details = {} self.invoices = set() self.skip_total_row = 0 @@ -84,6 +82,9 @@ class ReceivablePayableReport(object): self.total_row_map = {} self.skip_total_row = 1 + if self.filters.get("in_party_currency"): + self.skip_total_row = 1 + def get_data(self): self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() @@ -117,7 +118,7 @@ class ReceivablePayableReport(object): for ple in self.ple_entries: # get the balance object for voucher_type - if self.filters.get("ingore_accounts"): + 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) @@ -145,7 +146,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.init_subtotal_row(ple.party) - if self.filters.get("group_by_party"): + if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"): self.init_subtotal_row("Total") def get_invoices(self, ple): @@ -188,7 +189,7 @@ class ReceivablePayableReport(object): ): return - if self.filters.get("ingore_accounts"): + 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) @@ -200,7 +201,7 @@ 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: - if self.filters.get("ingore_accounts"): + 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) @@ -209,7 +210,7 @@ class ReceivablePayableReport(object): if not row: # no invoice, this is an invoice / stand-alone payment / credit note - if self.filters.get("ingore_accounts"): + 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)) @@ -224,8 +225,7 @@ class ReceivablePayableReport(object): if not row: return - # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -256,8 +256,10 @@ class ReceivablePayableReport(object): def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) - for field in self.get_currency_fields(): - total_row[field] += row.get(field, 0.0) + if total_row: + for field in self.get_currency_fields(): + total_row[field] += row.get(field, 0.0) + total_row["currency"] = row.get("currency", "") def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) @@ -281,11 +283,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: @@ -309,7 +320,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get("Total")) + self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): self.allocate_future_payments(row) @@ -440,7 +451,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): row.currency = row.account_currency else: row.currency = self.company_currency @@ -753,7 +764,12 @@ class ReceivablePayableReport(object): ) if self.filters.get("show_remarks"): - query = query.select(ple.remarks) + if remarks_length := frappe.db.get_single_value( + "Accounts Settings", "receivable_payable_remarks_length" + ): + query = query.select(Substring(ple.remarks, 1, remarks_length).as_("remarks")) + else: + query = query.select(ple.remarks) if self.filters.get("group_by_party"): query = query.orderby(self.ple.party, self.ple.posting_date) @@ -840,7 +856,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") @@ -1132,3 +1154,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 cbeb6d3106..dd0842df04 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 = ( @@ -557,6 +581,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) 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 5ad10c7890..2f6d2582b3 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/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 60274cd8b1..d50cf0708e 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport +from erpnext.accounts.utils import get_party_types_from_account_type def execute(filters=None): @@ -22,9 +23,7 @@ def execute(filters=None): class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): self.account_type = args.get("account_type") - self.party_type = frappe.db.get_all( - "Party Type", {"account_type": self.account_type}, pluck="name" - ) + self.party_type = get_party_types_from_account_type(self.account_type) self.party_naming_by = frappe.db.get_value( args.get("naming_by")[0], None, args.get("naming_by")[1] ) 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 126cd03795..12b94347e0 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 0ef9d858dd..9002e23ed3 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 f21c94b494..d285f28d8e 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 c2b57f768f..b05e744ae0 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -17,7 +17,7 @@ frappe.query_reports["Balance Sheet"]["filters"].push({ 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 6b8ed27e64..ef17eb1503 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -17,7 +17,7 @@ frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); 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/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index 590408c6f8..0e0c42dad9 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -104,7 +104,7 @@ frappe.query_reports["Consolidated Financial Statement"] = { }, { "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 693725d8f5..096bb10706 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_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index c0b4f59579..4cb443cf92 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 }, diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 5e484cf558..fa557a133f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -164,7 +164,12 @@ def get_gl_entries(filters, accounting_dimensions): credit_in_account_currency """ if filters.get("show_remarks"): - select_fields += """,remarks""" + if remarks_length := frappe.db.get_single_value( + "Accounts Settings", "general_ledger_remarks_length" + ): + select_fields += f",substr(remarks, 1, {remarks_length}) as 'remarks'" + else: + select_fields += """,remarks""" order_by_statement = "order by posting_date, account, creation" @@ -259,9 +264,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: diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index e842d2e8dc..f6c7bd3db7 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -184,6 +184,16 @@ def get_columns(filters): "width": 180, } ) + else: + columns.append( + { + "label": _(filters.get("party_type")), + "fieldname": "party", + "fieldtype": "Dynamic Link", + "options": "party_type", + "width": 180, + } + ) columns.extend( [ @@ -316,7 +326,7 @@ 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", + title=_("Accounts Missing Error"), ) gle = frappe.qb.DocType("GL Entry") query = ( diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index edd40b68ef..2c4c762073 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -95,7 +95,7 @@ frappe.query_reports["Trial Balance"] = { }, { "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 2a8aa0c202..8b7f0bbc00 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 0edfc2ad5a..380a04426f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -53,6 +53,9 @@ GL_REPOSTING_CHUNK = 100 def get_fiscal_year( date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False ): + if isinstance(boolean, str): + boolean = frappe.json.loads(boolean) + fiscal_years = get_fiscal_years( date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean ) @@ -180,6 +183,7 @@ def get_balance_on( cost_center=None, ignore_account_permission=False, account_type=None, + start_date=None, ): if not account and frappe.form_dict.get("account"): account = frappe.form_dict.get("account") @@ -193,6 +197,8 @@ def get_balance_on( cost_center = frappe.form_dict.get("cost_center") cond = ["is_cancelled=0"] + if start_date: + cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date))) if date: cond.append("posting_date <= %s" % frappe.db.escape(cstr(date))) else: @@ -1831,6 +1837,28 @@ class QueryPaymentLedger(object): Table("outstanding").amount_in_account_currency >= self.max_outstanding ) + if self.limit and self.get_invoices: + outstanding_vouchers = ( + qb.from_(ple) + .select( + ple.against_voucher_no.as_("voucher_no"), + Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"), + ) + .where(ple.delinked == 0) + .where(Criterion.all(filter_on_against_voucher_no)) + .where(Criterion.all(self.common_filter)) + .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party) + .orderby(ple.posting_date, ple.voucher_no) + .having(qb.Field("amount_in_account_currency") > 0) + .limit(self.limit) + .run() + ) + if outstanding_vouchers: + filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers])) + filter_on_against_voucher_no.append( + ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers]) + ) + # build query for voucher amount query_voucher_amount = ( qb.from_(ple) @@ -2047,3 +2075,7 @@ def create_gain_loss_journal( journal_entry.save() journal_entry.submit() return journal_entry.name + + +def get_party_types_from_account_type(account_type): + return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name") diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 40f51ab570..540a4f5549 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -481,11 +481,11 @@ "read_only": 1 }, { - "depends_on": "eval.doc.asset_quantity", + "default": "1", "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only": 1 + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "depr_entry_posting_status", @@ -572,7 +572,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-10-27 17:03:46.629617", + "modified": "2023-11-20 20:57:37.010467", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8908d8e5d0..12dcc5bcf3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -46,12 +46,28 @@ class Asset(AccountsController): self.validate_item() self.validate_cost_center() self.set_missing_values() - self.validate_finance_books() - if not self.split_from: - self.prepare_depreciation_data() - update_draft_asset_depr_schedules(self) self.validate_gross_and_purchase_amount() self.validate_expected_value_after_useful_life() + self.validate_finance_books() + + if not self.split_from: + self.prepare_depreciation_data() + + if self.calculate_depreciation: + update_draft_asset_depr_schedules(self) + + if frappe.db.exists("Asset", self.name): + asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self) + + if asset_depr_schedules_names: + asset_depr_schedules_links = get_comma_separated_links( + asset_depr_schedules_names, "Asset Depreciation Schedule" + ) + frappe.msgprint( + _( + "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." + ).format(asset_depr_schedules_links) + ) self.status = self.get_status() @@ -61,17 +77,7 @@ class Asset(AccountsController): if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() if self.calculate_depreciation and not self.split_from: - asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self) convert_draft_asset_depr_schedules_into_active(self) - if asset_depr_schedules_names: - asset_depr_schedules_links = get_comma_separated_links( - asset_depr_schedules_names, "Asset Depreciation Schedule" - ) - frappe.msgprint( - _( - "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." - ).format(asset_depr_schedules_links) - ) self.set_status() add_asset_activity(self.name, _("Asset submitted")) @@ -827,6 +833,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), "depreciation_start_date": d.depreciation_start_date or nowdate(), + "rate_of_depreciation": d.rate_of_depreciation, } ) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 84a428ca54..66930c0e7c 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -509,6 +509,9 @@ def restore_asset(asset_name): def depreciate_asset(asset_doc, date, notes): + if not asset_doc.calculate_depreciation: + return + asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones( @@ -521,6 +524,9 @@ def depreciate_asset(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, date, notes): + if not asset_doc.calculate_depreciation: + return + asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones( 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 48d33314ec..812b7f78e1 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 383be97347..45811a9344 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/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js index 0073170a85..dc54d606e7 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js @@ -1,30 +1,21 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bulk Transaction Log', { - - refresh: function(frm) { - frm.disable_save(); - frm.add_custom_button(__('Retry Failed Transactions'), ()=>{ - frappe.confirm(__("Retry Failing Transactions ?"), ()=>{ - query(frm, 1); - } - ); - }); - } +frappe.ui.form.on("Bulk Transaction Log", { + refresh(frm) { + frm.add_custom_button(__('Succeeded Entries'), function() { + frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"}); + }, __("View")); + frm.add_custom_button(__('Failed Entries'), function() { + frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Failed"}); + }, __("View")); + if (frm.doc.failed) { + frm.add_custom_button(__('Retry Failed Transactions'), function() { + frappe.call({ + method: "erpnext.utilities.bulk_transaction.retry", + args: {date: frm.doc.date} + }); + }); + } + }, }); - -function query(frm) { - frappe.call({ - method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", - args: { - log_date: frm.doc.log_date - } - }).then((r) => { - if (r.message === "No Failed Records") { - frappe.show_alert(__(r.message), 5); - } else { - frappe.show_alert(__("Retrying Failed Transactions"), 5); - } - }); -} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json index da42cf1bd4..75cb358ff2 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json @@ -1,31 +1,64 @@ { "actions": [], - "allow_rename": 1, - "creation": "2021-11-30 13:41:16.343827", + "allow_copy": 1, + "creation": "2023-11-09 20:14:45.139593", + "default_view": "List", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "log_date", - "logger_data" + "date", + "column_break_bsan", + "log_entries", + "section_break_mdmv", + "succeeded", + "column_break_qryp", + "failed" ], "fields": [ { - "fieldname": "log_date", + "fieldname": "date", "fieldtype": "Date", - "label": "Log Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Date", "read_only": 1 }, { - "fieldname": "logger_data", - "fieldtype": "Table", - "label": "Logger Data", - "options": "Bulk Transaction Log Detail" + "fieldname": "log_entries", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Log Entries", + "read_only": 1 + }, + { + "fieldname": "column_break_bsan", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_mdmv", + "fieldtype": "Section Break" + }, + { + "fieldname": "succeeded", + "fieldtype": "Int", + "label": "Succeeded", + "read_only": 1 + }, + { + "fieldname": "column_break_qryp", + "fieldtype": "Column Break" + }, + { + "fieldname": "failed", + "fieldtype": "Int", + "label": "Failed", + "read_only": 1 } ], - "index_web_pages_for_search": 1, + "in_create": 1, + "is_virtual": 1, "links": [], - "modified": "2022-02-03 17:23:02.935325", + "modified": "2023-11-11 04:52:49.347376", "modified_by": "Administrator", "module": "Bulk Transaction", "name": "Bulk Transaction Log", @@ -47,5 +80,5 @@ "sort_field": "modified", "sort_order": "DESC", "states": [], - "track_changes": 1 + "title_field": "date" } \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py index 0596be4462..712caf1f91 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py @@ -1,67 +1,112 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from datetime import date - import frappe +from frappe import qb from frappe.model.document import Document - -from erpnext.utilities.bulk_transaction import task, update_logger +from frappe.query_builder.functions import Count +from frappe.utils import cint +from pypika import Order class BulkTransactionLog(Document): - pass + def db_insert(self, *args, **kwargs): + pass + def load_from_db(self): + log_detail = qb.DocType("Bulk Transaction Log Detail") -@frappe.whitelist() -def retry_failing_transaction(log_date=None): - if not log_date: - log_date = str(date.today()) - btp = frappe.qb.DocType("Bulk Transaction Log Detail") - data = ( - frappe.qb.from_(btp) - .select(btp.transaction_name, btp.from_doctype, btp.to_doctype) - .distinct() - .where(btp.retried != 1) - .where(btp.transaction_status == "Failed") - .where(btp.date == log_date) - ).run(as_dict=True) + has_records = frappe.db.sql( + f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');" + )[0][0] + if not has_records: + raise frappe.DoesNotExistError - if data: - if len(data) > 10: - frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date) - else: - job(data, log_date) - else: - return "No Failed Records" + succeeded_logs = ( + qb.from_(log_detail) + .select(Count(log_detail.date).as_("count")) + .where((log_detail.date == self.name) & (log_detail.transaction_status == "Success")) + .run() + )[0][0] or 0 + failed_logs = ( + qb.from_(log_detail) + .select(Count(log_detail.date).as_("count")) + .where((log_detail.date == self.name) & (log_detail.transaction_status == "Failed")) + .run() + )[0][0] or 0 + total_logs = succeeded_logs + failed_logs + transaction_log = frappe._dict( + { + "date": self.name, + "count": total_logs, + "succeeded": succeeded_logs, + "failed": failed_logs, + } + ) + super(Document, self).__init__(serialize_transaction_log(transaction_log)) + @staticmethod + def get_list(args): + filter_date = parse_list_filters(args) + limit = cint(args.get("page_length")) or 20 + log_detail = qb.DocType("Bulk Transaction Log Detail") -def job(data, log_date): - for d in data: - failed = [] - try: - frappe.db.savepoint("before_creation_of_record") - task(d.transaction_name, d.from_doctype, d.to_doctype) - except Exception as e: - frappe.db.rollback(save_point="before_creation_of_record") - failed.append(e) - update_logger( - d.transaction_name, - e, - d.from_doctype, - d.to_doctype, - status="Failed", - log_date=log_date, - restarted=1, + dates_query = ( + qb.from_(log_detail) + .select(log_detail.date) + .distinct() + .orderby(log_detail.date, order=Order.desc) + .limit(limit) + ) + if filter_date: + dates_query = dates_query.where(log_detail.date == filter_date) + dates = dates_query.run() + + transaction_logs = [] + if dates: + transaction_logs_query = ( + qb.from_(log_detail) + .select(log_detail.date.as_("date"), Count(log_detail.date).as_("count")) + .where(log_detail.date.isin(dates)) + .orderby(log_detail.date, order=Order.desc) + .groupby(log_detail.date) + .limit(limit) ) + transaction_logs = transaction_logs_query.run(as_dict=True) - if not failed: - update_logger( - d.transaction_name, - None, - d.from_doctype, - d.to_doctype, - status="Success", - log_date=log_date, - restarted=1, - ) + return [serialize_transaction_log(x) for x in transaction_logs] + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + + +def serialize_transaction_log(data): + return frappe._dict( + name=data.date, + date=data.date, + log_entries=data.count, + succeeded=data.succeeded, + failed=data.failed, + ) + + +def parse_list_filters(args): + # parse date filter + filter_date = None + for fil in args.get("filters"): + if isinstance(fil, list): + for elem in fil: + if elem == "date": + filter_date = fil[3] + return filter_date diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py index c673be89b3..01bb615a3e 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py @@ -1,79 +1,9 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest -from datetime import date - -import frappe - -from erpnext.utilities.bulk_transaction import transaction_processing +# import frappe +from frappe.tests.utils import FrappeTestCase -class TestBulkTransactionLog(unittest.TestCase): - def setUp(self): - create_company() - create_customer() - create_item() - - def test_entry_in_log(self): - so_name = create_so() - transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") - doc = frappe.get_doc("Bulk Transaction Log", str(date.today())) - for d in doc.get("logger_data"): - if d.transaction_name == so_name: - self.assertEqual(d.transaction_name, so_name) - self.assertEqual(d.transaction_status, "Success") - self.assertEqual(d.from_doctype, "Sales Order") - self.assertEqual(d.to_doctype, "Sales Invoice") - self.assertEqual(d.retried, 0) - - -def create_company(): - if not frappe.db.exists("Company", "_Test Company"): - frappe.get_doc( - { - "doctype": "Company", - "company_name": "_Test Company", - "country": "India", - "default_currency": "INR", - } - ).insert() - - -def create_customer(): - if not frappe.db.exists("Customer", "Bulk Customer"): - frappe.get_doc({"doctype": "Customer", "customer_name": "Bulk Customer"}).insert() - - -def create_item(): - if not frappe.db.exists("Item", "MK"): - frappe.get_doc( - { - "doctype": "Item", - "item_code": "MK", - "item_name": "Milk", - "description": "Milk", - "item_group": "Products", - } - ).insert() - - -def create_so(intent=None): - so = frappe.new_doc("Sales Order") - so.customer = "Bulk Customer" - so.company = "_Test Company" - so.transaction_date = date.today() - - so.set_warehouse = "Finished Goods - _TC" - so.append( - "items", - { - "item_code": "MK", - "delivery_date": date.today(), - "qty": 10, - "rate": 80, - }, - ) - so.insert() - so.submit() - return so.name +class TestBulkTransactionLog(FrappeTestCase): + pass diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.js new file mode 100644 index 0000000000..5669601d11 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.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("Bulk Transaction Log Detail", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json index 8262caa020..9590325a06 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json @@ -6,12 +6,12 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "from_doctype", "transaction_name", "date", "time", "transaction_status", "error_description", - "from_doctype", "to_doctype", "retried" ], @@ -20,8 +20,11 @@ "fieldname": "transaction_name", "fieldtype": "Dynamic Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Name", - "options": "from_doctype" + "options": "from_doctype", + "read_only": 1, + "search_index": 1 }, { "fieldname": "transaction_status", @@ -39,9 +42,11 @@ { "fieldname": "from_doctype", "fieldtype": "Link", + "in_standard_filter": 1, "label": "From Doctype", "options": "DocType", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "to_doctype", @@ -54,8 +59,10 @@ "fieldname": "date", "fieldtype": "Date", "in_list_view": 1, + "in_standard_filter": 1, "label": "Date ", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "time", @@ -66,19 +73,33 @@ { "fieldname": "retried", "fieldtype": "Int", + "in_list_view": 1, "label": "Retried", "read_only": 1 } ], + "in_create": 1, "index_web_pages_for_search": 1, - "istable": 1, "links": [], - "modified": "2022-02-03 19:57:31.650359", + "modified": "2023-11-10 11:44:10.758342", "modified_by": "Administrator", "module": "Bulk Transaction", "name": "Bulk Transaction Log Detail", "owner": "Administrator", - "permissions": [], + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/test_bulk_transaction_log_detail.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/test_bulk_transaction_log_detail.py new file mode 100644 index 0000000000..5217b601f8 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/test_bulk_transaction_log_detail.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 TestBulkTransactionLogDetail(FrappeTestCase): + pass diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 059999245d..0af93bfc90 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -17,6 +17,7 @@ "po_required", "pr_required", "blanket_order_allowance", + "project_update_frequency", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -172,6 +173,14 @@ "fieldname": "blanket_order_allowance", "fieldtype": "Float", "label": "Blanket Order Allowance (%)" + }, + { + "default": "Each Transaction", + "description": "How often should Project be updated of Total Purchase Cost ?", + "fieldname": "project_update_frequency", + "fieldtype": "Select", + "label": "Update frequency of Project", + "options": "Each Transaction\nManual" } ], "icon": "fa fa-cog", @@ -179,7 +188,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-25 14:03:32.520418", + "modified": "2023-11-24 10:55:51.287327", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", 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 b1da97d634..2d706f41e5 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -189,6 +189,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -470,6 +471,7 @@ "fieldname": "material_request", "fieldtype": "Link", "label": "Material Request", + "mandatory_depends_on": "eval: doc.material_request_item", "no_copy": 1, "oldfieldname": "prevdoc_docname", "oldfieldtype": "Link", @@ -485,6 +487,7 @@ "fieldtype": "Data", "hidden": 1, "label": "Material Request Item", + "mandatory_depends_on": "eval: doc.material_request", "no_copy": 1, "oldfieldname": "prevdoc_detail_docname", "oldfieldtype": "Data", @@ -914,7 +917,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-27 15:50:42.655573", + "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/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 06dbd86ba1..fd73f77ff8 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,6 +9,8 @@ "field_order": [ "naming_series", "company", + "billing_address", + "billing_address_display", "vendor", "column_break1", "transaction_date", @@ -292,13 +294,25 @@ "fieldtype": "Check", "label": "Send Document Print", "print_hide": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Company Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address Details", + "read_only": 1 } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-09 12:20:26.850623", + "modified": "2023-11-06 12:45:28.898706", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", 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 82fcfa2713..6cdd2bac0d 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 @@ -87,6 +87,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -260,13 +261,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.py b/erpnext/buying/doctype/supplier/supplier.py index 31bf439dbb..b052f564a4 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -165,16 +165,17 @@ class Supplier(TransactionBase): @frappe.validate_and_sanitize_search_inputs def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): supplier = filters.get("supplier") - return frappe.db.sql( - """ - SELECT - `tabContact`.name from `tabContact`, - `tabDynamic Link` - WHERE - `tabContact`.name = `tabDynamic Link`.parent - and `tabDynamic Link`.link_name = %(supplier)s - and `tabDynamic Link`.link_doctype = 'Supplier' - and `tabContact`.name like %(txt)s - """, - {"supplier": supplier, "txt": "%%%s%%" % txt}, - ) + contact = frappe.qb.DocType("Contact") + dynamic_link = frappe.qb.DocType("Dynamic Link") + + return ( + frappe.qb.from_(contact) + .join(dynamic_link) + .on(contact.name == dynamic_link.parent) + .select(contact.name, contact.email_id) + .where( + (dynamic_link.link_name == supplier) + & (dynamic_link.link_doctype == "Supplier") + & (contact.name.like("%{0}%".format(txt))) + ) + ).run(as_dict=False) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 7b635b36ba..18912610ce 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -20,6 +20,10 @@ "valid_till", "quotation_number", "amended_from", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "currency_and_price_list", "currency", "conversion_rate", @@ -79,6 +83,7 @@ "pricing_rule_details", "pricing_rules", "address_and_contact_tab", + "supplier_address_section", "supplier_address", "address_display", "column_break_72", @@ -86,6 +91,14 @@ "contact_display", "contact_mobile", "contact_email", + "shipping_address_section", + "shipping_address", + "column_break_zjaq", + "shipping_address_display", + "company_billing_address_section", + "billing_address", + "column_break_gcth", + "billing_address_display", "terms_tab", "tc_name", "terms", @@ -838,6 +851,76 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "column_break_zjaq", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address Details", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" + }, + { + "fieldname": "supplier_address_section", + "fieldtype": "Section Break", + "label": "Supplier Address" + }, + { + "fieldname": "company_billing_address_section", + "fieldtype": "Section Break", + "label": "Company Billing Address" + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Company Billing Address", + "options": "Address" + }, + { + "fieldname": "column_break_gcth", + "fieldtype": "Column Break" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address Details", + "read_only": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "icon": "fa fa-shopping-cart", @@ -845,7 +928,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:20:15.880114", + "modified": "2023-11-17 12:34:30.083077", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", 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 8d491fbc84..a6229b5950 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -68,6 +68,8 @@ "column_break_15", "manufacturer_part_no", "ad_sec_break", + "cost_center", + "dimension_col_break", "project", "section_break_44", "page_break" @@ -553,19 +555,31 @@ "fieldname": "expected_delivery_date", "fieldtype": "Date", "label": "Expected Delivery Date" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-19 12:36:26.913211", + "modified": "2023-11-17 12:25:26.235367", "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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c9c248c3ea..154d490646 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -239,7 +239,7 @@ class AccountsController(TransactionBase): references_map.setdefault(x.parent, []).append(x.name) for doc, rows in references_map.items(): - unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc) for row in rows: unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) @@ -248,9 +248,9 @@ class AccountsController(TransactionBase): unreconcile_doc.save(ignore_permissions=True) # delete docs upon parent doc deletion - unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) + unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name}) for x in unreconcile_docs: - _doc = frappe.get_doc("Unreconcile Payments", x.name) + _doc = frappe.get_doc("Unreconcile Payment", x.name) if _doc.docstatus == 1: _doc.cancel() _doc.delete() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3a802bd26f..68ad97d7ba 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -105,26 +105,26 @@ class BuyingController(SubcontractingController): def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: for row in self.items: + if row.rate <= 0: + # override the rate with valuation rate + row.rate = get_incoming_rate( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.get("posting_date"), + "posting_time": self.get("posting_time"), + "qty": row.qty, + "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + }, + raise_error_if_no_rate=False, + ) - # override the rate with valuation rate - row.rate = get_incoming_rate( - { - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.get("posting_date"), - "posting_time": self.get("posting_time"), - "qty": row.qty, - "serial_and_batch_bundle": row.get("serial_and_batch_bundle"), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - }, - raise_error_if_no_rate=False, - ) - - row.discount_percentage = 0.0 - row.discount_amount = 0.0 - row.margin_rate_or_amount = 0.0 + row.discount_percentage = 0.0 + row.discount_amount = 0.0 + row.margin_rate_or_amount = 0.0 def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -365,7 +365,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_and_batch_bundle": d.get("serial_and_batch_bundle"), @@ -758,7 +758,7 @@ class BuyingController(SubcontractingController): "calculate_depreciation": 0, "purchase_receipt_amount": purchase_amount, "gross_purchase_amount": purchase_amount, - "asset_quantity": row.qty if is_grouped_asset else 0, + "asset_quantity": row.qty if is_grouped_asset else 1, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, } diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 5ec24743d9..199732b152 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -611,6 +611,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/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 165e17b2d7..e91212b031 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -356,6 +356,7 @@ def make_return_doc( if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice": doc.consolidated_invoice = "" doc.set("payments", []) + doc.update_billed_amount_in_delivery_note = True for data in source.payments: paid_amount = 0.00 base_paid_amount = 0.00 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d34fbeb0da..5575a24b35 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -350,11 +350,12 @@ class SellingController(StockController): return il def has_product_bundle(self, item_code): - return frappe.db.sql( - """select name from `tabProduct Bundle` - where new_item_code=%s and docstatus != 2""", - item_code, - ) + product_bundle = frappe.qb.DocType("Product Bundle") + return ( + frappe.qb.from_(product_bundle) + .select(product_bundle.name) + .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) + ).run() def get_already_delivered_qty(self, current_docname, so, so_detail): delivered_via_dn = frappe.db.sql( diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 5fa66b1a87..3d55a087bd 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -626,6 +626,18 @@ class SubcontractingController(StockController): (row.item_code, row.get(self.subcontract_data.order_field)) ] -= row.qty + def __set_rate_for_serial_and_batch_bundle(self): + if self.doctype != "Subcontracting Receipt": + return + + for row in self.get(self.raw_material_table): + if not row.get("serial_and_batch_bundle"): + continue + + row.rate = frappe.get_cached_value( + "Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate" + ) + def __modify_serial_and_batch_bundle(self): if self.is_new(): return @@ -681,6 +693,7 @@ class SubcontractingController(StockController): self.__remove_changed_rows() self.__set_supplied_items() self.__modify_serial_and_batch_bundle() + self.__set_rate_for_serial_and_batch_bundle() def __validate_batch_no(self, row, key): if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 96284d612f..f9f68a119b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -54,6 +54,7 @@ class calculate_taxes_and_totals(object): if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): self.doc.grand_total -= self.doc.discount_amount self.doc.base_grand_total -= self.doc.base_discount_amount + self.doc.rounding_adjustment = self.doc.base_rounding_adjustment = 0.0 self.set_rounded_total() self.calculate_shipping_charges() diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index e897ba41eb..fdec88d70d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -7,6 +7,8 @@ from frappe.contacts.address_and_contact import ( delete_contact_and_address, load_address_and_contact, ) +from frappe.contacts.doctype.address.address import get_default_address +from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address @@ -251,6 +253,13 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): target.customer_group = frappe.db.get_default("Customer Group") + address = get_default_address("Lead", source.name) + contact = get_default_contact("Lead", source.name) + if address: + target.customer_primary_address = address + if contact: + target.customer_primary_contact = contact + doclist = get_mapped_doc( "Lead", source_name, diff --git a/erpnext/crm/doctype/opportunity_item/opportunity_item.json b/erpnext/crm/doctype/opportunity_item/opportunity_item.json index 1b4973c1b2..732f80d01c 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/hooks.py b/erpnext/hooks.py index 5483a10b57..c6ab6f12f6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -421,7 +421,7 @@ scheduler_events = { "hourly_long": [ "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", - "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", + "erpnext.utilities.bulk_transaction.retry", ], "daily": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", @@ -539,6 +539,8 @@ accounting_dimension_doctypes = [ "Subcontracting Receipt", "Subcontracting Receipt Item", "Account Closing Balance", + "Supplier Quotation", + "Supplier Quotation Item", ] get_matching_queries = ( diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js index 0cf2b51df2..243e52df5b 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -15,7 +15,7 @@ frappe.ui.form.on("BOM Creator", { || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { frm.trigger("build_tree"); } - } else { + } else if (!frm.doc.items?.length ) { let $parent = $(frm.fields_dict["bom_creator"].wrapper); $parent.empty(); frm.trigger("make_new_entry"); diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 058caa3686..49041a0929 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -6,7 +6,7 @@ from collections import OrderedDict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt +from frappe.utils import cint, flt from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate @@ -91,11 +91,19 @@ class BOMCreator(Document): parent_reference = {row.idx: row.name for row in self.items} for row in self.items: - if row.fg_reference_id: + ref_id = "" + + if row.parent_row_no: + ref_id = parent_reference.get(cint(row.parent_row_no)) + + # Check whether the reference id of the FG Item has correct or not + if row.fg_reference_id and row.fg_reference_id == ref_id: continue if row.parent_row_no: - row.fg_reference_id = parent_reference.get(row.parent_row_no) + row.fg_reference_id = ref_id + elif row.fg_item == self.item_code: + row.fg_reference_id = self.name @frappe.whitelist() def add_boms(self): diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index fdb5d3ad33..56acd8a1a6 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -215,7 +215,6 @@ "fieldname": "parent_row_no", "fieldtype": "Data", "label": "Parent Row No", - "no_copy": 1, "print_hide": 1 }, { @@ -231,7 +230,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-07 11:52:30.492233", + "modified": "2023-11-16 13:34:06.321061", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator Item", 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 9b1db63494..c75ac32cd1 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 c5266119dc..cb58af1f29 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/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 72438ddcee..dd102b0fae 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.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6b12a29b50..6efb762905 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -828,8 +828,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) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index e9c6ee3af2..dd32c34358 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -664,49 +664,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." diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 58945bba77..d9cc212e8c 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/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js index 521543ab1b..afe4a6e0cb 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 2c8f82f2cc..076690ff09 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 e0f32c55da..a73502de5b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -259,6 +259,7 @@ erpnext.patches.v14_0.update_reference_due_date_in_journal_entry erpnext.patches.v15_0.saudi_depreciation_warning erpnext.patches.v15_0.delete_saudi_doctypes erpnext.patches.v14_0.show_loan_management_deprecation_warning +erpnext.patches.v14_0.clear_reconciliation_values_from_singles execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) [post_model_sync] @@ -338,15 +339,18 @@ erpnext.patches.v15_0.delete_woocommerce_settings_doctype erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.update_invoicing_period_in_subscription execute:frappe.delete_doc("Page", "welcome-to-erpnext") +erpnext.patches.v15_0.migrate_payment_request_status erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v15_0.update_sre_from_voucher_details erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field -execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) -execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) +erpnext.patches.v14_0.add_default_for_repost_settings erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v15_0.set_reserved_stock_in_bin +erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation +erpnext.patches.v14_0.update_zero_asset_quantity_field +execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index e53bdf8f19..08ddbbf337 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 @@ -21,6 +21,9 @@ def execute(): params = set({x.casefold(): x for x in params}.values()) for parameter in params: + if frappe.db.exists("Quality Inspection Parameter", parameter): + continue + frappe.get_doc( {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v14_0/add_default_for_repost_settings.py b/erpnext/patches/v14_0/add_default_for_repost_settings.py new file mode 100644 index 0000000000..6cafc66aab --- /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/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py new file mode 100644 index 0000000000..c1f5b60a40 --- /dev/null +++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py @@ -0,0 +1,17 @@ +from frappe import qb + + +def execute(): + """ + Clear `tabSingles` and Payment Reconciliation tables of values + """ + singles = qb.DocType("Singles") + qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run() + doctypes = [ + "Payment Reconciliation Invoice", + "Payment Reconciliation Payment", + "Payment Reconciliation Allocation", + ] + for x in doctypes: + dt = qb.DocType(x) + qb.from_(dt).delete().run() diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_supplier_quotation.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_supplier_quotation.py new file mode 100644 index 0000000000..6966db1fd7 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_supplier_quotation.py @@ -0,0 +1,8 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Supplier Quotation") + create_accounting_dimensions_for_doctype(doctype="Supplier Quotation Item") diff --git a/erpnext/patches/v14_0/update_zero_asset_quantity_field.py b/erpnext/patches/v14_0/update_zero_asset_quantity_field.py new file mode 100644 index 0000000000..0480f9b7aa --- /dev/null +++ b/erpnext/patches/v14_0/update_zero_asset_quantity_field.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + asset = frappe.qb.DocType("Asset") + frappe.qb.update(asset).set(asset.asset_quantity, 1).where(asset.asset_quantity == 0).run() diff --git a/erpnext/patches/v15_0/migrate_payment_request_status.py b/erpnext/patches/v15_0/migrate_payment_request_status.py new file mode 100644 index 0000000000..9f0de56621 --- /dev/null +++ b/erpnext/patches/v15_0/migrate_payment_request_status.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + """ + Description: + Change Inward Payment Requests from statut 'Initiated' to correct status 'Requested'. + Status 'Initiated' is reserved for Outward Payment Requests and was a semantic error in previour versions. + """ + so = frappe.qb.DocType("Payment Request") + frappe.qb.update(so).set(so.status, "Requested").where(so.payment_request_type == "Inward").where( + so.status == "Initiated" + ).run() diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index f366f77556..2dac399d88 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -68,6 +68,10 @@ frappe.ui.form.on("Project", { frm.events.create_duplicate(frm); }, __("Actions")); + frm.add_custom_button(__('Update Total Purchase Cost'), () => { + frm.events.update_total_purchase_cost(frm); + }, __("Actions")); + frm.trigger("set_project_status_button"); @@ -92,6 +96,22 @@ frappe.ui.form.on("Project", { }, + update_total_purchase_cost: function(frm) { + frappe.call({ + method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost", + args: {project: frm.doc.name}, + freeze: true, + freeze_message: __('Recalculating Purchase Cost against this Project...'), + callback: function(r) { + if (r && !r.exc) { + frappe.msgprint(__('Total Purchase Cost has been updated')); + frm.refresh(); + } + } + + }); + }, + set_project_status_button: function(frm) { frm.add_custom_button(__('Set Project Status'), () => { let d = new frappe.ui.Dialog({ diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index e9aed1afb4..4f2e39539d 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -4,11 +4,11 @@ import frappe from email_reply_parser import EmailReplyParser -from frappe import _ +from frappe import _, qb from frappe.desk.reportview import get_match_cond from frappe.model.document import Document from frappe.query_builder import Interval -from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp +from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today from frappe.utils.user import is_website_user @@ -249,12 +249,7 @@ class Project(Document): self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 def update_purchase_costing(self): - total_purchase_cost = frappe.db.sql( - """select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = %s and docstatus=1""", - self.name, - ) - + total_purchase_cost = calculate_total_purchase_cost(self.name) self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0 def update_sales_amount(self): @@ -695,3 +690,29 @@ def get_holiday_list(company=None): def get_users_email(doc): return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")] + + +def calculate_total_purchase_cost(project: str | None = None): + if project: + pitem = qb.DocType("Purchase Invoice Item") + frappe.qb.DocType("Purchase Invoice Item") + total_purchase_cost = ( + qb.from_(pitem) + .select(Sum(pitem.base_net_amount)) + .where((pitem.project == project) & (pitem.docstatus == 1)) + .run(as_list=True) + ) + return total_purchase_cost + return None + + +@frappe.whitelist() +def recalculate_project_total_purchase_cost(project: str | None = None): + if project: + total_purchase_cost = calculate_total_purchase_cost(project) + frappe.db.set_value( + "Project", + project, + "total_purchase_cost", + (total_purchase_cost and total_purchase_cost[0][0] or 0), + ) diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 25a5455ac1..4d2d225242 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -57,6 +57,7 @@ ], "fields": [ { + "allow_in_quick_entry": 1, "fieldname": "subject", "fieldtype": "Data", "in_global_search": 1, @@ -66,6 +67,7 @@ "search_index": 1 }, { + "allow_in_quick_entry": 1, "bold": 1, "fieldname": "project", "fieldtype": "Link", @@ -396,7 +398,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2023-09-28 13:52:05.861175", + "modified": "2023-11-20 11:42:41.884069", "modified_by": "Administrator", "module": "Projects", "name": "Task", @@ -416,6 +418,7 @@ "write": 1 } ], + "quick_entry": 1, "search_fields": "subject", "show_name_in_global_search": 1, "show_preview_popup": 1, diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index d1d07a79d6..eb7a97e615 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -111,6 +111,7 @@ frappe.ui.form.on("Timesheet", { frm.trigger('setup_filters'); frm.trigger('set_dynamic_field_label'); + frm.trigger('set_route_options_for_new_task'); }, customer: function(frm) { @@ -172,6 +173,14 @@ frappe.ui.form.on("Timesheet", { frm.refresh_fields(); }, + set_route_options_for_new_task: (frm) => { + let task_field = frm.get_docfield('time_logs', 'task'); + + if (task_field) { + task_field.get_route_options_for_new_doc = (row) => ({'project': row.doc.project}); + } + }, + make_invoice: function(frm) { let fields = [{ "fieldtype": "Link", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 11156f4b50..b9d801ce90 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -71,6 +71,12 @@ class Timesheet(Document): if args.is_billable: if flt(args.billing_hours) == 0.0: args.billing_hours = args.hours + elif flt(args.billing_hours) > flt(args.hours): + frappe.msgprint( + _("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx), + indicator="orange", + alert=True, + ) else: args.billing_hours = 0 diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6b613ce9ec..d24c4e6075 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -43,6 +43,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (this.frm.doc.apply_discount_on == "Grand Total" && this.frm.doc.is_cash_or_non_trade_discount) { this.frm.doc.grand_total -= this.frm.doc.discount_amount; this.frm.doc.base_grand_total -= this.frm.doc.base_discount_amount; + this.frm.doc.rounding_adjustment = 0; + this.frm.doc.base_rounding_adjustment = 0; + this.set_rounded_total(); } await this.calculate_shipping_charges(); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b0a9e405cd..2c40f4964b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1772,7 +1772,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) { if (!me.frm.doc[fieldname]) { frappe.msgprint(__("Please specify") + ": " + - frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name) + + __(frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name)) + ". " + __("It is needed to fetch Item Details.")); valid = false; } diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 907a775bfa..1b10d8ad3a 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -139,7 +139,6 @@ function get_filters() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, @@ -148,7 +147,6 @@ function get_filters() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, @@ -197,5 +195,13 @@ function get_filters() { } ] + // Dynamically set 'default' values for fiscal year filters + let fy_filters = filters.filter(x=>{return ["from_fiscal_year", "to_fiscal_year"].includes(x.fieldname);}) + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), false, true); + if (fiscal_year) { + let fy = erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), false, false); + fy_filters.forEach(x=>{x.default = fy;}) + } + return filters; } diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index d435711cf5..25fc754b9a 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -404,7 +404,7 @@ $.extend(erpnext.utils, { }); }, - get_fiscal_year: function(date, with_dates=false) { + get_fiscal_year: function(date, with_dates=false, boolean=false) { if(!date) { date = frappe.datetime.get_today(); } @@ -413,7 +413,8 @@ $.extend(erpnext.utils, { frappe.call({ method: "erpnext.accounts.utils.get_fiscal_year", args: { - date: date + date: date, + boolean: boolean }, async: false, callback: function(r) { diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 5c41aa0680..cba615c0d2 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.utils"); const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; -const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; +const PURCHASE_DOCTYPES = ['Supplier Quotation','Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; erpnext.utils.get_party_details = function(frm, method, args, callback) { if (!method) { diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index fa00ed2362..79490a162d 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -1,6 +1,6 @@ frappe.provide('erpnext.accounts'); -erpnext.accounts.unreconcile_payments = { +erpnext.accounts.unreconcile_payment = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) @@ -10,7 +10,7 @@ erpnext.accounts.unreconcile_payments = { } frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references", "args": { "doctype": frm.doc.doctype, "docname": frm.doc.name @@ -18,7 +18,7 @@ erpnext.accounts.unreconcile_payments = { callback: function(r) { if (r.message) { frm.add_custom_button(__("UnReconcile"), function() { - erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); + erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm); }, __('Actions')); } } @@ -74,7 +74,7 @@ erpnext.accounts.unreconcile_payments = { // get linked payments frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc", "args": { "company": frm.doc.company, "doctype": frm.doc.doctype, @@ -96,8 +96,8 @@ erpnext.accounts.unreconcile_payments = { let selected_allocations = values.allocations.filter(x=>x.__checked); if (selected_allocations.length > 0) { - let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations); - erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map); + let selection_map = erpnext.accounts.unreconcile_payment.build_selection_map(frm, selected_allocations); + erpnext.accounts.unreconcile_payment.create_unreconcile_docs(selection_map); d.hide(); } else { @@ -115,7 +115,7 @@ erpnext.accounts.unreconcile_payments = { create_unreconcile_docs(selection_map) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection", "args": { "selections": selection_map }, diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index fd2b6a4eaa..79fd2ebdbe 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 e8604080fb..6834abc9d4 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 04e8211214..467186debd 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.js b/erpnext/selling/doctype/customer/customer.js index 42932ad8bd..ddc7e2af8f 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -3,17 +3,32 @@ frappe.ui.form.on("Customer", { setup: function(frm) { - + frm.custom_make_buttons = { + "Opportunity": "Opportunity", + "Quotation": "Quotation", + "Sales Order": "Sales Order", + "Pricing Rule": "Pricing Rule", + }; frm.make_methods = { - 'Quotation': () => frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.customer.customer.make_quotation", - frm: cur_frm - }), - 'Opportunity': () => frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.customer.customer.make_opportunity", - frm: cur_frm - }) - } + "Quotation": () => + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_quotation", + frm: frm, + }), + "Sales Order": () => + frappe.model.with_doctype("Sales Order", function () { + var so = frappe.model.get_new_doc("Sales Order"); + so.customer = frm.doc.name; // Set the current customer as the SO customer + frappe.set_route("Form", "Sales Order", so.name); + }), + "Opportunity": () => + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_opportunity", + frm: frm, + }), + "Pricing Rule": () => + erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name), + }; frm.add_fetch('lead_name', 'company_name', 'customer_name'); frm.add_fetch('default_sales_partner','commission_rate','default_commission_rate'); @@ -146,9 +161,9 @@ frappe.ui.form.on("Customer", { {party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name}); }, __('View')); - frm.add_custom_button(__('Pricing Rule'), function () { - erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); - }, __('Create')); + for (const doctype in frm.make_methods) { + frm.add_custom_button(__(doctype), frm.make_methods[doctype], __("Create")); + } frm.add_custom_button(__('Get Customer Group Details'), function () { frm.trigger("get_customer_group_details"); diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a7a1aa2659..88ed1c6667 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -186,6 +186,8 @@ class Customer(TransactionBase): self.db_set("customer_primary_contact", contact.name) self.db_set("mobile_no", self.mobile_no) self.db_set("email_id", self.email_id) + elif self.customer_primary_contact: + frappe.set_value("Contact", self.customer_primary_contact, "is_primary_contact", 1) # ensure def create_primary_address(self): from frappe.contacts.doctype.address.address import get_address_display @@ -196,6 +198,8 @@ class Customer(TransactionBase): self.db_set("customer_primary_address", address.name) self.db_set("primary_address", address_display) + elif self.customer_primary_address: + frappe.set_value("Address", self.customer_primary_address, "is_primary_address", 1) # ensure def update_lead_status(self): """If Customer created from Lead, update lead status to "Converted" @@ -303,22 +307,6 @@ class Customer(TransactionBase): ) -def create_contact(contact, party_type, party, email): - """Create contact based on given contact name""" - contact = contact.split(" ") - - contact = frappe.get_doc( - { - "doctype": "Contact", - "first_name": contact[0], - "last_name": len(contact) > 1 and contact[1] or "", - } - ) - contact.append("email_ids", dict(email_id=email, is_primary=1)) - contact.append("links", dict(link_doctype=party_type, link_name=party)) - contact.insert() - - @frappe.whitelist() def make_quotation(source_name, target_doc=None): def set_missing_values(source, target): @@ -495,6 +483,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, @@ -635,24 +624,47 @@ def get_credit_limit(customer, company): def make_contact(args, is_primary_contact=1): - contact = frappe.get_doc( - { - "doctype": "Contact", - "first_name": args.get("name"), - "is_primary_contact": is_primary_contact, - "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], - } - ) + values = { + "doctype": "Contact", + "is_primary_contact": is_primary_contact, + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + + party_type = args.customer_type if args.doctype == "Customer" else args.supplier_type + party_name_key = "customer_name" if args.doctype == "Customer" else "supplier_name" + + if party_type == "Individual": + first, middle, last = parse_full_name(args.get(party_name_key)) + values.update( + { + "first_name": first, + "middle_name": middle, + "last_name": last, + } + ) + else: + values.update( + { + "company_name": args.get(party_name_key), + } + ) + + contact = frappe.get_doc(values) + if args.get("email_id"): contact.add_email(args.get("email_id"), is_primary=True) if args.get("mobile_no"): contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) - contact.insert() + + if flags := args.get("flags"): + contact.insert(ignore_permissions=flags.get("ignore_permissions")) + else: + contact.insert() return contact -def make_address(args, is_primary_address=1): +def make_address(args, is_primary_address=1, is_shipping_address=1): reqd_fields = [] for field in ["city", "country"]: if not args.get(field): @@ -665,19 +677,28 @@ def make_address(args, is_primary_address=1): title=_("Missing Values Required"), ) + party_name_key = "customer_name" if args.doctype == "Customer" else "supplier_name" + address = frappe.get_doc( { "doctype": "Address", - "address_title": args.get("name"), + "address_title": args.get(party_name_key), "address_line1": args.get("address_line1"), "address_line2": args.get("address_line2"), "city": args.get("city"), "state": args.get("state"), "pincode": args.get("pincode"), "country": args.get("country"), + "is_primary_address": is_primary_address, + "is_shipping_address": is_shipping_address, "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], } - ).insert() + ) + + if flags := args.get("flags"): + address.insert(ignore_permissions=flags.get("ignore_permissions")) + else: + address.insert() return address @@ -698,3 +719,13 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .run() ) + + +def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]: + """Parse full name into first name, middle name and last name""" + names = full_name.split() + first_name = names[0] + middle_name = " ".join(names[1:-1]) if len(names) > 2 else None + last_name = names[-1] if len(names) > 1 else None + + return first_name, middle_name, last_name diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 6e737e4b55..29dbd4f321 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -10,7 +10,11 @@ from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen -from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding +from erpnext.selling.doctype.customer.customer import ( + get_credit_limit, + get_customer_outstanding, + parse_full_name, +) from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] @@ -373,6 +377,22 @@ class TestCustomer(FrappeTestCase): frappe.db.set_single_value("Selling Settings", "cust_master_name", "Customer Name") + def test_parse_full_name(self): + first, middle, last = parse_full_name("John") + self.assertEqual(first, "John") + self.assertEqual(middle, None) + self.assertEqual(last, None) + + first, middle, last = parse_full_name("John Doe") + self.assertEqual(first, "John") + self.assertEqual(middle, None) + self.assertEqual(last, "Doe") + + first, middle, last = parse_full_name("John Michael Doe") + self.assertEqual(first, "John") + self.assertEqual(middle, "Michael") + self.assertEqual(last, "Doe") + def get_customer_dict(customer_name): return { diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index 56155fb750..c4f21b61b9 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -1,315 +1,119 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "beta": 0, - "creation": "2013-06-20 11:53:21", - "custom": 0, - "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "creation": "2013-06-20 11:53:21", + "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "basic_section", + "new_item_code", + "description", + "column_break_eonk", + "disabled", + "item_section", + "items", + "section_break_4", + "about" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "basic_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": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "fieldname": "basic_section", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "new_item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Parent Item", - "length": 0, - "no_copy": 1, - "oldfieldname": "new_item_code", - "oldfieldtype": "Data", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "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, - "unique": 0 - }, + "fieldname": "new_item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Parent Item", + "no_copy": 1, + "oldfieldname": "new_item_code", + "oldfieldtype": "Data", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Data", - "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": "Description", - "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, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "List items that form the package.", - "fieldname": "item_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": "Items", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "List items that form the package.", + "fieldname": "item_section", + "fieldtype": "Section Break", + "label": "Items" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "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": "Items", - "length": 0, - "no_copy": 0, - "oldfieldname": "sales_bom_items", - "oldfieldtype": "Table", - "options": "Product Bundle Item", - "permlevel": 0, - "print_hide": 0, - "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, - "unique": 0 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "sales_bom_items", + "oldfieldtype": "Table", + "options": "Product Bundle Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "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, - "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, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "about", - "fieldtype": "HTML", - "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": "", - "length": 0, - "no_copy": 0, - "options": "

About Product Bundle

\n\n

Aggregate group of Items into another Item. This is useful if you are bundling a certain Items into a package and you maintain stock of the packed Items and not the aggregate Item.

\n

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

\n

Example:

\n

If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.

", - "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, - "unique": 0 + "fieldname": "about", + "fieldtype": "HTML", + "options": "

About Product Bundle

\n\n

Aggregate group of Items into another Item. This is useful if you are bundling a certain Items into a package and you maintain stock of the packed Items and not the aggregate Item.

\n

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

\n

Example:

\n

If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.

" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_eonk", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-sitemap", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Product Bundle", - "owner": "Administrator", + ], + "icon": "fa fa-sitemap", + "idx": 1, + "links": [], + "modified": "2023-11-22 15:20:46.805114", + "modified_by": "Administrator", + "module": "Selling", + "name": "Product Bundle", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index ac83c0f046..2fd9cc1301 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -59,10 +59,12 @@ class ProductBundle(Document): """Validates, main Item is not a stock item""" if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code)) + if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"): + frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code)) def validate_child_items(self): for item in self.items: - if frappe.db.exists("Product Bundle", item.item_code): + if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}): frappe.throw( _( "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" @@ -73,12 +75,17 @@ class ProductBundle(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - - return frappe.db.sql( - """select name, item_name, description from tabItem - where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s offset %s""" - % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), - ("%%%s%%" % txt, page_len, start), - ) + product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") + item = frappe.qb.DocType("Item") + return ( + frappe.qb.from_(item) + .select("*") + .where( + (item.is_stock_item == 0) + & (item.is_fixed_asset == 0) + & (item.name.notin(product_bundles)) + & (item[searchfield].like(f"%{txt}%")) + ) + .limit(page_len) + .offset(start) + ).run() diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 5016f1f1fd..0e25313f76 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -135,6 +135,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -666,7 +667,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-09-26 13:42:11.410294", + "modified": "2023-11-14 18:24:24.619832", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", @@ -676,4 +677,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e4f1a28316..a23599b180 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -688,7 +688,9 @@ def make_material_request(source_name, target_doc=None): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + "condition": lambda item: not frappe.db.exists( + "Product Bundle", {"name": item.item_code, "disabled": 0} + ) and get_remaining_qty(item) > 0, "postprocess": update_item, }, @@ -767,8 +769,11 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): if target.company_address: target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) - # set target items names to ensure proper linking with packed_items - target.set_new_name() + # if invoked in bulk creation, validations are ignored and thus this method is nerver invoked + if frappe.flags.bulk_transaction: + # set target items names to ensure proper linking with packed_items + target.set_new_name() + make_packing_list(target) def condition(doc): @@ -1306,7 +1311,7 @@ def set_delivery_date(items, sales_order): def is_product_bundle(item_code): - return frappe.db.exists("Product Bundle", item_code) + return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}) @frappe.whitelist() @@ -1518,7 +1523,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0): product_bundle_parents = [ pb.new_item_code for pb in frappe.get_all( - "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + "Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"] ) ] 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 f82047f511..b4f73003ae 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -68,7 +68,6 @@ "total_weight", "column_break_21", "weight_uom", - "accounting_dimensions_section", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -177,6 +176,7 @@ "print_hide": 1 }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -890,18 +890,12 @@ "label": "Production Plan Qty", "no_copy": 1, "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-17 18:18:26.475259", + "modified": "2023-11-14 18:37:12.787893", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index db6255a4be..feecd9cfd8 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -548,6 +548,14 @@ erpnext.PointOfSale.Controller = class { if (!item_code) return; + if (rate == undefined || rate == 0) { + frappe.show_alert({ + message: __('Price is not set for the item.'), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + return; + } const new_item = { item_code, batch_no, rate, uom, [field]: value }; if (serial_no) { @@ -601,11 +609,12 @@ erpnext.PointOfSale.Controller = class { // if item is clicked twice from item selector // then "item_code, batch_no, uom, rate" will help in getting the exact item // to increase the qty by one - const has_batch_no = batch_no; + const has_batch_no = (batch_no !== 'null' && batch_no !== null); item_row = this.frm.doc.items.find( i => i.item_code === item_code && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) && (i.uom === uom) + && (i.rate === flt(rate)) ); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 89ce61ab16..63711c5ed2 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -203,7 +203,7 @@ erpnext.PointOfSale.Payment = class { const paid_amount = doc.paid_amount; const items = doc.items; - if (paid_amount == 0 || !items.length) { + if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) { const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order."); frappe.show_alert({ message, indicator: "orange" }); frappe.utils.play_sound("error"); diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 4542bdff43..0a29d435a4 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -26,7 +26,7 @@ def execute(filters=None): def get_columns(filters): party_type = filters.get("party_type") party_type_value = get_party_group(party_type) - return [ + columns = [ "{party_type}:Link/{party_type}".format(party_type=party_type), "{party_value_type}::150".format(party_value_type=frappe.unscrub(str(party_type_value))), "Address Line 1", @@ -43,6 +43,15 @@ def get_columns(filters): "Email Id", "Is Primary Contact:Check", ] + if filters.get("party_type") == "Supplier" and frappe.db.get_single_value( + "Buying Settings", "supp_master_name" + ) == ["Naming Series", "Auto Name"]: + columns.insert(1, "Supplier Name:Data:150") + if filters.get("party_type") == "Customer" and frappe.db.get_single_value( + "Selling Settings", "cust_master_name" + ) == ["Naming Series", "Auto Name"]: + columns.insert(1, "Customer Name:Data:150") + return columns def get_data(filters): @@ -50,27 +59,33 @@ def get_data(filters): party = filters.get("party_name") party_group = get_party_group(party_type) - return get_party_addresses_and_contact(party_type, party, party_group) + return get_party_addresses_and_contact(party_type, party, party_group, filters) -def get_party_addresses_and_contact(party_type, party, party_group): +def get_party_addresses_and_contact(party_type, party, party_group, filters): data = [] - filters = None + query_filters = None party_details = frappe._dict() if not party_type: return [] if party: - filters = {"name": party} + query_filters = {"name": party} + if filters.get("party_type") in ["Customer", "Supplier"]: + field = filters.get("party_type").lower() + "_name" + else: + field = "partner_name" fetch_party_list = frappe.get_list( - party_type, filters=filters, fields=["name", party_group], as_list=True + party_type, filters=query_filters, fields=["name", party_group, field], as_list=True ) party_list = [d[0] for d in fetch_party_list] party_groups = {} + party_name_map = {} for d in fetch_party_list: party_groups[d[0]] = d[1] + party_name_map[d[0]] = d[2] for d in party_list: party_details.setdefault(d, frappe._dict()) @@ -84,6 +99,8 @@ def get_party_addresses_and_contact(party_type, party, party_group): if not any([addresses, contacts]): result = [party] result.append(party_groups[party]) + if filters.get("party_type") in ["Customer", "Supplier"]: + result.append(party_name_map[party]) result.extend(add_blank_columns_for("Contact")) result.extend(add_blank_columns_for("Address")) data.append(result) @@ -95,11 +112,12 @@ def get_party_addresses_and_contact(party_type, party, party_group): for idx in range(0, max_length): result = [party] result.append(party_groups[party]) + if filters.get("party_type") in ["Customer", "Supplier"]: + result.append(party_name_map[party]) address = addresses[idx] if idx < len(addresses) else add_blank_columns_for("Address") contact = contacts[idx] if idx < len(contacts) else add_blank_columns_for("Contact") result.extend(address) result.extend(contact) - data.append(result) return data @@ -115,7 +133,6 @@ def get_party_details(party_type, party_list, doctype, party_details): for d in records: details = party_details.get(d[0]) details.setdefault(frappe.scrub(doctype), []).append(d[1:]) - return party_details 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 5dfc1db097..ecb63d890a 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/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 4fc20e6103..6ed44fff68 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -382,9 +382,10 @@ class EmailDigest(Document): """Get income to date""" balance = 0.0 count = 0 + fy_start_date = get_fiscal_year(self.future_to_date)[1] for account in self.get_root_type_accounts(root_type): - balance += get_balance_on(account, date=self.future_to_date) + balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date) count += get_count_on(account, fieldname, date=self.future_to_date) if fieldname == "income": diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 39a215f383..efc3fd1d33 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 78fb4dfc58..6f9176cf27 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -48,6 +48,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): @@ -230,12 +233,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): @@ -347,6 +364,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 5a693c5eff..9b70683626 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 481a3a5ebe..d266285b29 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 e6cb3516a3..e20030a568 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": "2023-03-12 15:56:09.516586", + "modified": "2023-11-09 12:17:28.339975", "modified_by": "Administrator", "module": "Stock", "name": "Batch", @@ -224,7 +225,6 @@ "read": 1, "report": 1, "role": "Item Manager", - "set_user_permissions": 1, "share": 1, "write": 1 } 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 225da6d15e..0c4757ffad 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/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 66dd33a400..f240136e9c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -615,7 +615,7 @@ class DeliveryNote(SellingController): items_list = [item.item_code for item in self.items] return frappe.db.get_all( "Product Bundle", - filters={"new_item_code": ["in", items_list]}, + filters={"new_item_code": ["in", items_list], "disabled": 0}, pluck="name", ) @@ -938,7 +938,7 @@ def make_packing_slip(source_name, target_doc=None): }, "postprocess": update_item, "condition": lambda item: ( - not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) and flt(item.packed_qty) < flt(item.qty) ), }, 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 6148950462..a44b9ac44b 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -168,6 +168,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -893,7 +894,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/item/item.json b/erpnext/stock/doctype/item/item.json index c13d3ebe0f..13f3be8c36 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -504,7 +504,7 @@ "fieldtype": "Table", "hidden": 1, "label": "Variant Attributes", - "mandatory_depends_on": "has_variants", + "mandatory_depends_on": "eval:(doc.has_variants || doc.variant_of) && doc.variant_based_on==='Item Attribute'", "options": "Item Variant Attribute" }, { @@ -888,7 +888,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-09-11 13:46:32.688051", + "modified": "2023-09-18 15:41:32.688051", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d8935fe203..cb34497f28 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -512,8 +512,12 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name}) - new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name}) + old_bundle = frappe.get_value( + "Product Bundle", filters={"new_item_code": old_name, "disabled": 0} + ) + new_bundle = frappe.get_value( + "Product Bundle", filters={"new_item_code": new_name, "disabled": 0} + ) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 09d3dd1dad..a942f58bd6 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -163,7 +163,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": "", + "item_tax_template": None, }, { "item_code": "_Test Item Inherit Group Item Tax Template 1", @@ -178,7 +178,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", - "item_tax_template": "", + "item_tax_template": None, }, { "item_code": "_Test Item Inherit Group Item Tax Template 2", @@ -193,7 +193,7 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", - "item_tax_template": "", + "item_tax_template": None, }, { "item_code": "_Test Item Override Group Item Tax Template", @@ -208,12 +208,12 @@ class TestItem(FrappeTestCase): { "item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": "", + "item_tax_template": None, }, ] expected_item_tax_map = { - "": {}, + None: {}, "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10}, "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, 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 9912be145f..5dc07c99f6 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -110,6 +110,7 @@ "width": "250px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach Image", "label": "Image", @@ -478,7 +479,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-27 15:53:41.444236", + "modified": "2023-11-14 18:37:59.599115", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index a9e9ad1a63..35701c90de 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -55,7 +55,7 @@ def make_packing_list(doc): def is_product_bundle(item_code: str) -> bool: - return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code})) + return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0})) def get_indexed_packed_items_table(doc): @@ -111,7 +111,7 @@ def get_product_bundle_items(item_code): product_bundle_item.uom, product_bundle_item.description, ) - .where(product_bundle.new_item_code == item_code) + .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) .orderby(product_bundle_item.idx) ) return query.run(as_dict=True) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ed20209577..644df3d29a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -368,7 +368,9 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) if not cint( frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + ) and not frappe.db.exists( + "Product Bundle", {"new_item_code": item.item_code, "disabled": 0} + ): continue item_code = item.item_code reference = item.sales_order_item or item.material_request_item @@ -507,7 +509,9 @@ class PickList(Document): # bundle_item_code: Dict[component, qty] product_bundle_qty_map = {} for bundle_item_code in bundles: - bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code}) + bundle = frappe.get_last_doc( + "Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0} + ) product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1fcde44053..e02dfedc7d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -7,7 +7,7 @@ from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import CombineDatetime -from frappe.utils import cint, flt, getdate, nowdate +from frappe.utils import cint, flt, get_datetime, getdate, nowdate from pypika import functions as fn import erpnext @@ -734,12 +734,18 @@ class PurchaseReceipt(BuyingController): def update_assets(self, item, valuation_rate): assets = frappe.db.get_all( - "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} + "Asset", + filters={"purchase_receipt": self.name, "item_code": item.item_code}, + fields=["name", "asset_quantity"], ) for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity + ) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity + ) def update_status(self, status): self.set_status(update=True, status=status) @@ -763,8 +769,12 @@ class PurchaseReceipt(BuyingController): update_billing_percentage(pr_doc, update_modified=update_modified) def reserve_stock_for_sales_order(self): - if self.is_return or not cint( - frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase") + if ( + self.is_return + or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation") + or not frappe.db.get_single_value( + "Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase" + ) ): return @@ -785,6 +795,11 @@ class PurchaseReceipt(BuyingController): so_items_details_map.setdefault(item.sales_order, []).append(item_details) if so_items_details_map: + if get_datetime("{} {}".format(self.posting_date, self.posting_time)) > get_datetime(): + return frappe.msgprint( + _("Cannot create Stock Reservation Entries for future dated Purchase Receipts.") + ) + for so, items_details in so_items_details_map.items(): so_doc = frappe.get_doc("Sales Order", so) so_doc.create_stock_reservation_entries( 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 718f007577..ce2e5d7f84 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -192,6 +192,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -1090,7 +1091,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-30 17:32:24.560337", + "modified": "2023-11-14 18:38:15.251994", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index f2bbf2b211..abdbeb496b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -246,7 +246,7 @@ class SerialandBatchBundle(Document): valuation_field = "rate" child_table = "Subcontracting Receipt Supplied Item" else: - valuation_field = "rm_supp_cost" + valuation_field = "rate" child_table = "Subcontracting Receipt Item" precision = frappe.get_precision(child_table, valuation_field) or 2 @@ -406,7 +406,7 @@ class SerialandBatchBundle(Document): if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01: self.throw_error_message( - f"Total quantity {abs(self.total_qty)} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(row.get(qty_field))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" + f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" ) def set_is_outward(self): @@ -1604,6 +1604,9 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: ) for key, val in kwargs.items(): + if not val: + continue + if key in ["get_subcontracted_item"]: continue diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e87658bc98..b9e1af5d18 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -161,7 +161,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage" ) ) self.queue_action("submit", timeout=2000) @@ -172,7 +172,7 @@ class StockEntry(StockController): if self.is_enqueue_action(): frappe.msgprint( _( - "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage" ) ) self.queue_action("cancel", timeout=2000) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 569f58a69f..be379940ca 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,8 @@ "warehouse", "posting_date", "posting_time", + "is_adjustment_entry", + "auto_created_serial_and_batch_bundle", "column_break_6", "voucher_type", "voucher_no", @@ -333,6 +335,19 @@ "fieldname": "has_serial_no", "fieldtype": "Check", "label": "Has Serial No" + }, + { + "default": "0", + "fieldname": "is_adjustment_entry", + "fieldtype": "Check", + "label": "Is Adjustment Entry" + }, + { + "default": "0", + "depends_on": "serial_and_batch_bundle", + "fieldname": "auto_created_serial_and_batch_bundle", + "fieldtype": "Check", + "label": "Auto Created Serial and Batch Bundle" } ], "hide_toolbar": 1, @@ -341,7 +356,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-03 16:33:16.270722", + "modified": "2023-11-14 16:47:39.791967", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 323ad4f57d..1bf143b49c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -442,6 +442,11 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: + + if not row.qty and not row.valuation_rate and not row.current_qty: + self.make_adjustment_entry(row, sl_entries) + continue + item = frappe.get_cached_value( "Item", row.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 ) @@ -492,6 +497,21 @@ class StockReconciliation(StockController): ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + 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 get_sle_for_serialized_items(self, row, sl_entries): if row.current_serial_and_batch_bundle: args = self.get_sle_for_items(row) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e29fc882ce..d1a9cf26ac 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -149,7 +149,7 @@ def remove_standard_fields(details): def set_valuation_rate(out, args): - if frappe.db.exists("Product Bundle", args.item_code, cache=True): + if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True): valuation_rate = 0.0 bundled_items = frappe.get_doc("Product Bundle", args.item_code) @@ -610,7 +610,6 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): # all templates have validity and no template is valid if not taxes_with_validity and (not taxes_with_no_validity): - out["item_tax_template"] = "" return None # do not change if already a valid template diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index ab47b4a8b9..a53a9f24f5 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -202,7 +202,7 @@ def get_valuation_rate(): bin_data = ( frappe.qb.from_(bin) .select( - bin.item_code, Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty).as_("val_rate") + bin.item_code, (Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty)).as_("val_rate") ) .where(bin.actual_qty > 0) .groupby(bin.item_code) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 5998274bb7..da98455b5c 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -129,7 +129,9 @@ class SerialBatchBundle: frappe.throw(_(error_msg)) def set_serial_and_batch_bundle(self, sn_doc): - self.sle.db_set("serial_and_batch_bundle", sn_doc.name) + self.sle.db_set( + {"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1} + ) if sn_doc.is_rejected: frappe.db.set_value( @@ -143,6 +145,12 @@ class SerialBatchBundle: @property def child_doctype(self): child_doctype = self.sle.voucher_type + " Item" + + if ( + self.sle.voucher_type == "Subcontracting Receipt" and self.sle.dependant_sle_voucher_detail_no + ): + child_doctype = "Subcontracting Receipt Supplied Item" + if self.sle.voucher_type == "Stock Entry": child_doctype = "Stock Entry Detail" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e9381d42b9..9142a27f4c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -759,12 +759,16 @@ 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"): + if not self.args.get("sle_id") or ( + sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle + ): self.update_outgoing_rate_on_transaction(sle) def reset_actual_qty_for_stock_reco(self, sle): @@ -1939,3 +1943,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/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index d77e77440e..46c229bfd3 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.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 19a1c939c3..36001eb78f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -13,6 +13,16 @@ frappe.ui.form.on('Subcontracting Receipt', { frm.trigger('set_queries'); }, + on_submit(frm) { + frm.events.refresh_serial_batch_bundle_field(frm); + }, + + refresh_serial_batch_bundle_field(frm) { + frappe.route_hooks.after_submit = (frm_obj) => { + frm_obj.reload_doc(); + } + }, + refresh: (frm) => { if (frm.doc.docstatus > 0) { frm.add_custom_button(__('Stock Ledger'), () => { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 8be1c1ba97..383a83b3fc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -11,6 +11,7 @@ "naming_series", "supplier", "supplier_name", + "supplier_delivery_note", "column_break1", "company", "posting_date", @@ -634,12 +635,17 @@ "fieldtype": "Button", "label": "Get Scrap Items", "options": "get_scrap_items" + }, + { + "fieldname": "supplier_delivery_note", + "fieldtype": "Data", + "label": "Supplier Delivery Note" } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-26 10:52:04.050829", + "modified": "2023-11-16 13:04:00.710534", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 7e06444e1e..8d705aa97d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -148,6 +148,8 @@ class SubcontractingReceipt(SubcontractingController): if ( frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") == "BOM" + and self.supplied_items + and not any(item.serial_and_batch_bundle for item in self.supplied_items) ): self.supplied_items = [] diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 1828f6960f..f0e4e00074 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -6,7 +6,7 @@ import copy import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, cint, cstr, flt, today +from frappe.utils import add_days, cint, cstr, flt, nowtime, today import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -26,6 +26,10 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries +from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + make_serial_batch_bundle, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -507,6 +511,260 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) + def test_subcontracting_receipt_for_batch_raw_materials_without_material_transfer(self): + set_backflush_based_on("BOM") + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1]) + + rm_batch_no = None + for row in bom.items: + se = make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + se.reload() + rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.reload() + + bundle_doc = make_serial_batch_bundle( + { + "item_code": scr.supplied_items[0].rm_item_code, + "warehouse": "_Test Warehouse 1 - _TC", + "voucher_type": "Subcontracting Receipt", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -1, + "batches": frappe._dict({rm_batch_no: 1}), + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + + scr.supplied_items[0].serial_and_batch_bundle = bundle_doc.name + scr.submit() + scr.reload() + + batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, rm_batch_no) + self.assertEqual(scr.items[0].rm_cost_per_qty, 300) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + + def test_subcontracting_receipt_valuation_with_auto_created_serial_batch_bundle(self): + set_backflush_based_on("BOM") + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + "serial_no_series": "BNSS-.####", + } + ).name + + rm_item3 = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "BSSSS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) + + rm_batch_no = None + for row in bom.items: + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=400, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + for row in scr.supplied_items: + self.assertNotEqual(row.rate, 300.00) + self.assertFalse(row.serial_and_batch_bundle) + + scr.submit() + scr.reload() + + for row in scr.supplied_items: + self.assertEqual(row.rate, 300.00) + self.assertTrue(row.serial_and_batch_bundle) + auto_created_serial_batch = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": row.name}, + "auto_created_serial_and_batch_bundle", + ) + + self.assertTrue(auto_created_serial_batch) + + self.assertEqual(scr.items[0].rm_cost_per_qty, 900) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 + ) + + def test_subcontracting_receipt_valuation_for_fg_with_auto_created_serial_batch_bundle(self): + set_backflush_based_on("BOM") + + fg_item = make_item( + properties={ + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BSSNGS-.####", + } + ).name + + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + "serial_no_series": "BNSS-.####", + } + ).name + + rm_item3 = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "BSSSS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) + + rm_batch_no = None + for row in bom.items: + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + scr.reload() + + for row in scr.supplied_items: + self.assertEqual(row.rate, 300.00) + self.assertTrue(row.serial_and_batch_bundle) + auto_created_serial_batch = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": row.name}, + "auto_created_serial_and_batch_bundle", + ) + + self.assertTrue(auto_created_serial_batch) + + self.assertEqual(scr.items[0].rm_cost_per_qty, 900) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + self.assertEqual(scr.items[0].rate, 1000) + valuation_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": scr.items[0].name}, + "valuation_rate", + ) + + self.assertEqual(flt(valuation_rate), flt(1000)) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 + ) + def test_subcontracting_receipt_raw_material_rate(self): # Step - 1: Set Backflush Based On as "BOM" set_backflush_based_on("BOM") 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 38432beb44..26a29dd811 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -109,6 +109,7 @@ "width": "300px" }, { + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach", "hidden": 1, @@ -521,7 +522,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/translations/af.csv b/erpnext/translations/af.csv index f45731468d..ee77d98948 100644 --- a/erpnext/translations/af.csv +++ b/erpnext/translations/af.csv @@ -1153,7 +1153,7 @@ In Value,In Waarde, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",In die geval van 'n multi-vlak program sal kliënte outomaties toegewys word aan die betrokke vlak volgens hul besteding, Inactive,onaktiewe, Incentives,aansporings, -Include Default Book Entries,Sluit standaardboekinskrywings in, +Include Default FB Entries,Sluit standaardboekinskrywings in, Include Exploded Items,Sluit ontplofte items in, Include POS Transactions,Sluit POS-transaksies in, Include UOM,Sluit UOM in, diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv index 0453d5d685..a5f09a7488 100644 --- a/erpnext/translations/am.csv +++ b/erpnext/translations/am.csv @@ -1153,7 +1153,7 @@ In Value,እሴት ውስጥ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","በባለብዙ ደረጃ መርሃግብር ሁኔታ, ደንበኞች በተጠቀሱት ወጪ መሰረት ለተሰጣቸው ደረጃ ደረጃ በራስ መተላለፍ ይኖራቸዋል", Inactive,ገባሪ አይደለም, Incentives,ማበረታቻዎች, -Include Default Book Entries,ነባሪ የመጽሐፍ ግቤቶችን አካትት።, +Include Default FB Entries,ነባሪ የመጽሐፍ ግቤቶችን አካትት።, Include Exploded Items,የተበተኑ ንጥሎችን አካት, Include POS Transactions,የ POS ሽግግሮችን አክል, Include UOM,UOM አካት, diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index 67b409e7ef..195b9c7963 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -1153,7 +1153,7 @@ In Value,القيمة القادمة, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",في حالة البرنامج متعدد المستويات ، سيتم تعيين العملاء تلقائيًا إلى الطبقة المعنية وفقًا للإنفاق, Inactive,غير نشط, Incentives,الحوافز, -Include Default Book Entries,تضمين إدخالات دفتر افتراضي, +Include Default FB Entries,تضمين إدخالات دفتر افتراضي, Include Exploded Items,تشمل البنود المستبعدة, Include POS Transactions,تشمل معاملات نقطه البيع, Include UOM,تضمين UOM, diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv index 787f81ee7f..c2bacf4f93 100644 --- a/erpnext/translations/bg.csv +++ b/erpnext/translations/bg.csv @@ -1153,7 +1153,7 @@ In Value,В стойност, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","В случай на многостепенна програма, клиентите ще бъдат автоматично зададени на съответния подреждан по тяхна сметка", Inactive,неактивен, Incentives,Стимули, -Include Default Book Entries,Включете записи по подразбиране на книги, +Include Default FB Entries,Включете записи по подразбиране на книги, Include Exploded Items,Включете експлодираните елементи, Include POS Transactions,Включете POS транзакции, Include UOM,Включете UOM, diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index 69fd08cb51..d7366e14ab 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -1153,7 +1153,7 @@ In Value,মান, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","মাল্টি-টিয়ার প্রোগ্রামের ক্ষেত্রে, গ্রাহকরা তাদের ব্যয় অনুযায়ী সংশ্লিষ্ট টায়ারে স্বয়ংক্রিয়ভাবে নিয়োগ পাবেন", Inactive,নিষ্ক্রিয়, Incentives,ইনসেনটিভ, -Include Default Book Entries,ডিফল্ট বুক এন্ট্রি অন্তর্ভুক্ত করুন, +Include Default FB Entries,ডিফল্ট বুক এন্ট্রি অন্তর্ভুক্ত করুন, Include Exploded Items,বিস্ফোরিত আইটেম অন্তর্ভুক্ত করুন, Include POS Transactions,পিওএস লেনদেন অন্তর্ভুক্ত করুন, Include UOM,UOM অন্তর্ভুক্ত করুন, diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv index ef680a36ad..df4083eed6 100644 --- a/erpnext/translations/bs.csv +++ b/erpnext/translations/bs.csv @@ -1153,7 +1153,7 @@ In Value,u vrijednost, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","U slučaju višeslojnog programa, Korisnici će automatski biti dodeljeni za dotičnu grupu po njihovom trošenju", Inactive,Neaktivan, Incentives,Poticaji, -Include Default Book Entries,Uključite zadane unose knjiga, +Include Default FB Entries,Uključite zadane unose knjiga, Include Exploded Items,Uključite eksplodirane predmete, Include POS Transactions,Uključite POS transakcije, Include UOM,Uključite UOM, diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv index fa545a4c19..b3cf2c5fe1 100644 --- a/erpnext/translations/ca.csv +++ b/erpnext/translations/ca.csv @@ -1153,7 +1153,7 @@ In Value,En valor, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","En el cas del programa de diversos nivells, els clients seran assignats automàticament al nivell corresponent segons el seu gastat", Inactive,Inactiu, Incentives,Incentius, -Include Default Book Entries,Inclou les entrades de llibres predeterminats, +Include Default FB Entries,Inclou les entrades de llibres predeterminats, Include Exploded Items,Inclou articles explotats, Include POS Transactions,Inclou transaccions de POS, Include UOM,Inclou UOM, diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv index 7fb1679f9c..b6deaa46d8 100644 --- a/erpnext/translations/cs.csv +++ b/erpnext/translations/cs.csv @@ -1153,7 +1153,7 @@ In Value,v Hodnota, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",V případě víceúrovňového programu budou zákazníci automaticky přiděleni danému vrstvě podle svých vynaložených nákladů, Inactive,Neaktivní, Incentives,Pobídky, -Include Default Book Entries,Zahrnout výchozí položky knihy, +Include Default FB Entries,Zahrnout výchozí položky knihy, Include Exploded Items,Zahrnout výbušné položky, Include POS Transactions,Zahrnout POS transakce, Include UOM,Zahrnout UOM, diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv index 4eb39609f2..4bcc307583 100644 --- a/erpnext/translations/da.csv +++ b/erpnext/translations/da.csv @@ -1153,7 +1153,7 @@ In Value,I Value, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","I tilfælde af multi-tier program, vil kunder automatisk blive tildelt den pågældende tier som per deres brugt", Inactive,inaktive, Incentives,Incitamenter, -Include Default Book Entries,Inkluder standardbogsindlæg, +Include Default FB Entries,Inkluder standardbogsindlæg, Include Exploded Items,Inkluder eksploderede elementer, Include POS Transactions,Inkluder POS-transaktioner, Include UOM,Inkluder UOM, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index c627f81984..73755be653 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -533,6 +533,7 @@ Company currencies of both the companies should match for Inter Company Transact Company is manadatory for company account,Bitte gib ein Unternehmen für dieses Unternehmenskonto an., Company name not same,Firma nicht gleich, Company {0} does not exist,Unternehmen {0} existiert nicht, +Competitor,Konkurrent, Compensatory leave request days not in valid holidays,"Tage des Ausgleichsurlaubs, die nicht in den gültigen Feiertagen sind", Complaint,Beschwerde, Completion Date,Fertigstellungstermin, @@ -1160,7 +1161,7 @@ In Value,Wert bei, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",Im Falle eines mehrstufigen Programms werden Kunden automatisch der betroffenen Ebene entsprechend ihrer Ausgaben zugewiesen, Inactive,Inaktiv, Incentives,Anreize, -Include Default Book Entries,Standardbucheinträge einschließen, +Include Default FB Entries,Standardbucheinträge einschließen, Include Exploded Items,Unterartikel einbeziehen, Include POS Transactions,POS-Transaktionen einschließen, Include UOM,Fügen Sie UOM hinzu, @@ -5073,7 +5074,7 @@ Percentage you are allowed to transfer more against the quantity ordered. For ex PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-, Get Items from Open Material Requests,Hole Artikel von offenen Material Anfragen, Fetch items based on Default Supplier.,Abrufen von Elementen basierend auf dem Standardlieferanten., -Required By,Benötigt von, +Required By,Benötigt bis, Order Confirmation No,Auftragsbestätigung Nr, Order Confirmation Date,Auftragsbestätigungsdatum, Customer Mobile No,Mobilnummer des Kunden, diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv index 21fb435901..e67eaffc69 100644 --- a/erpnext/translations/el.csv +++ b/erpnext/translations/el.csv @@ -1153,7 +1153,7 @@ In Value,στην Αξία, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Στην περίπτωση προγράμματος πολλαπλών βαθμίδων, οι Πελάτες θα αντιστοιχούν αυτόματα στη σχετική βαθμίδα σύμφωνα με το ποσό που δαπανώνται", Inactive,Αδρανής, Incentives,Κίνητρα, -Include Default Book Entries,Συμπεριλάβετε τις προεπιλεγμένες καταχωρίσεις βιβλίων, +Include Default FB Entries,Συμπεριλάβετε τις προεπιλεγμένες καταχωρίσεις βιβλίων, Include Exploded Items,Συμπεριλάβετε εκραγμένα στοιχεία, Include POS Transactions,Συμπεριλάβετε τις συναλλαγές POS, Include UOM,Συμπεριλάβετε UOM, diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index 2abe418707..0c90694f8f 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -1153,7 +1153,7 @@ In Value,En valor, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","En el caso del programa de varios niveles, los Clientes se asignarán automáticamente al nivel correspondiente según su gasto", Inactive,Inactivo, Incentives,Incentivos, -Include Default Book Entries,Incluir entradas de libro predeterminadas, +Include Default FB Entries,Incluir entradas de libro predeterminadas, Include Exploded Items,Incluir Elementos Estallados, Include POS Transactions,Incluir transacciones POS, Include UOM,Incluir UOM, diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv index a4a8736006..69a89d9d22 100644 --- a/erpnext/translations/et.csv +++ b/erpnext/translations/et.csv @@ -1153,7 +1153,7 @@ In Value,väärtuse, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",Mitmekordsete programmide korral määratakse Kliendid automaatselt asjaomasele tasemele vastavalt nende kasutatud kuludele, Inactive,Mitteaktiivne, Incentives,Soodustused, -Include Default Book Entries,Lisage vaikeraamatu kanded, +Include Default FB Entries,Lisage vaikeraamatu kanded, Include Exploded Items,Kaasa lõhutud esemed, Include POS Transactions,Kaasa POS-tehingud, Include UOM,Lisa UOM, diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index bd40c8b396..cddabfb448 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -1153,7 +1153,7 @@ In Value,با ارزش, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",در مورد برنامه چند لایه، مشتریان به صورت خودکار به سطر مربوطه اختصاص داده می شوند، همانطور که در هزینه های خود هستند, Inactive,غیر فعال, Incentives,انگیزه, -Include Default Book Entries,شامل ورودی های پیش فرض کتاب, +Include Default FB Entries,شامل ورودی های پیش فرض کتاب, Include Exploded Items,شامل موارد انفجار, Include POS Transactions,شامل معاملات POS, Include UOM,شامل UOM, diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv index 33cf1574ae..eae6053915 100644 --- a/erpnext/translations/fi.csv +++ b/erpnext/translations/fi.csv @@ -1153,7 +1153,7 @@ In Value,in Arvo, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",Monitasoisen ohjelman tapauksessa asiakkaat määräytyvät automaattisesti kyseiselle tasolle niiden kulutuksen mukaan, Inactive,Epäaktiivinen, Incentives,kannustimet/bonukset, -Include Default Book Entries,Sisällytä oletustiedot, +Include Default FB Entries,Sisällytä oletustiedot, Include Exploded Items,Sisällytä räjähtämättömiä kohteita, Include POS Transactions,Sisällytä POS-tapahtumia, Include UOM,Sisällytä UOM, diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index d15af74d47..5c34759a70 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -1058,7 +1058,7 @@ In Stock: ,En Stock :, In Value,En valeur, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Dans le cas d'un programme à plusieurs échelons, les clients seront automatiquement affectés au niveau approprié en fonction de leurs dépenses", Incentives,Incitations, -Include Default Book Entries,Inclure les entrées de livre par défaut, +Include Default FB Entries,Inclure les entrées de livre par défaut, Include Exploded Items,Inclure les articles éclatés, Include POS Transactions,Inclure les transactions du point de vente, Include UOM,Inclure UdM, diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv index 06a3cc64af..604ec411c4 100644 --- a/erpnext/translations/gu.csv +++ b/erpnext/translations/gu.csv @@ -1153,7 +1153,7 @@ In Value,ભાવ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","મલ્ટિ-ટાયર પ્રોગ્રામના કિસ્સામાં, ગ્રાહક તેમના ખર્ચ મુજબ સંબંધિત ટાયરમાં ઓટો હશે", Inactive,નિષ્ક્રિય, Incentives,ઇનસેન્ટીવ્સ, -Include Default Book Entries,ડિફaultલ્ટ બુક એન્ટ્રીઓ શામેલ કરો, +Include Default FB Entries,ડિફaultલ્ટ બુક એન્ટ્રીઓ શામેલ કરો, Include Exploded Items,વિસ્ફોટ થયેલ આઇટમ્સ શામેલ કરો, Include POS Transactions,POS વ્યવહારો શામેલ કરો, Include UOM,યુએમએમ શામેલ કરો, diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv index d5fcab6454..5407578f2b 100644 --- a/erpnext/translations/he.csv +++ b/erpnext/translations/he.csv @@ -1153,7 +1153,7 @@ In Value,ערך, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","במקרה של תוכנית רב-שכבתית, הלקוחות יוקצו אוטומטית לשכבה הנוגעת בדבר שהוצאו", Inactive,לֹא פָּעִיל, Incentives,תמריצים, -Include Default Book Entries,כלול רשומות ברירת מחדל לספרים, +Include Default FB Entries,כלול רשומות ברירת מחדל לספרים, Include Exploded Items,כלול פריטים מפוצצים, Include POS Transactions,כלול עסקאות קופה, Include UOM,כלול UOM, diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv index a5caa666c2..00532df0f1 100644 --- a/erpnext/translations/hi.csv +++ b/erpnext/translations/hi.csv @@ -1153,7 +1153,7 @@ In Value,मूल्य में, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","मल्टी-स्तरीय कार्यक्रम के मामले में, ग्राहक अपने खर्च के अनुसार संबंधित स्तर को स्वचालित रूप से सौंपा जाएगा", Inactive,निष्क्रिय, Incentives,प्रोत्साहन, -Include Default Book Entries,डिफ़ॉल्ट बुक प्रविष्टियाँ शामिल करें, +Include Default FB Entries,डिफ़ॉल्ट बुक प्रविष्टियाँ शामिल करें, Include Exploded Items,विस्फोट किए गए आइटम शामिल करें, Include POS Transactions,पीओएस लेनदेन शामिल करें, Include UOM,यूओएम शामिल करें, diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv index 2834602248..3cc9ef3be2 100644 --- a/erpnext/translations/hr.csv +++ b/erpnext/translations/hr.csv @@ -1153,7 +1153,7 @@ In Value,u vrijednost, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","U slučaju višerazinskog programa, Kupci će biti automatski dodijeljeni odgovarajućem stupcu po njihovu potrošenom", Inactive,neaktivan, Incentives,poticaji, -Include Default Book Entries,Uključite zadane unose u knjige, +Include Default FB Entries,Uključite zadane unose u knjige, Include Exploded Items,Uključi eksplodirane predmete, Include POS Transactions,Uključi POS transakcije, Include UOM,Uključi UOM, diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv index a262c8a994..42175bbe34 100644 --- a/erpnext/translations/hu.csv +++ b/erpnext/translations/hu.csv @@ -1153,7 +1153,7 @@ In Value,Az Értékben, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Többszintű program esetében az ügyfeleket automatikusan az adott kategóriába sorolják, az általuk elköltöttek szerint", Inactive,Inaktív, Incentives,Ösztönzők, -Include Default Book Entries,Tartalmazza az alapértelmezett könyvbejegyzéseket, +Include Default FB Entries,Tartalmazza az alapértelmezett könyvbejegyzéseket, Include Exploded Items,Tartalmazza a robbantott elemeket, Include POS Transactions,Tartalmazza a POS kassza tranzakciókat, Include UOM,Ide tartozik az ANYJ, diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv index c4e50fdfe5..d69eef389d 100644 --- a/erpnext/translations/id.csv +++ b/erpnext/translations/id.csv @@ -1153,7 +1153,7 @@ In Value,Nilai, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Dalam kasus program multi-tier, Pelanggan akan ditugaskan secara otomatis ke tingkat yang bersangkutan sesuai yang mereka habiskan", Inactive,Tidak aktif, Incentives,Insentif, -Include Default Book Entries,Sertakan Entri Buku Default, +Include Default FB Entries,Sertakan Entri Buku Default, Include Exploded Items,Sertakan barang yang meledak, Include POS Transactions,Sertakan Transaksi POS, Include UOM,Termasuk UOM, diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv index 50c06ecfbb..1acefbb3ee 100644 --- a/erpnext/translations/is.csv +++ b/erpnext/translations/is.csv @@ -1153,7 +1153,7 @@ In Value,Virði, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Þegar um er að ræða fjölþættaráætlun, verða viðskiptavinir sjálfkrafa tengdir viðkomandi flokka eftir því sem þeir eru í", Inactive,Óvirkt, Incentives,Incentives, -Include Default Book Entries,Hafa sjálfgefnar bókarfærslur með, +Include Default FB Entries,Hafa sjálfgefnar bókarfærslur með, Include Exploded Items,Inniheldur sprauta hluti, Include POS Transactions,Innifalið POS-viðskipti, Include UOM,Innifalið UOM, diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv index 37608958c3..e6e64254bb 100644 --- a/erpnext/translations/it.csv +++ b/erpnext/translations/it.csv @@ -1153,7 +1153,7 @@ In Value,In valore, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Nel caso di un programma multilivello, i clienti verranno assegnati automaticamente al livello interessato come da loro speso", Inactive,Inattivo, Incentives,Incentivi, -Include Default Book Entries,Includi voci di libro predefinite, +Include Default FB Entries,Includi voci di libro predefinite, Include Exploded Items,Includi elementi esplosi, Include POS Transactions,Includi transazioni POS, Include UOM,Includi UOM, diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv index 888ec800c9..dd5820ae77 100644 --- a/erpnext/translations/ja.csv +++ b/erpnext/translations/ja.csv @@ -1153,7 +1153,7 @@ In Value,値内, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",マルチティアプログラムの場合、顧客は、消費されるごとに自動的に関係する層に割り当てられます, Inactive,非アクティブ, Incentives,インセンティブ, -Include Default Book Entries,デフォルトのブックエントリを含める, +Include Default FB Entries,デフォルトのブックエントリを含める, Include Exploded Items,分解された項目を含める, Include POS Transactions,POSトランザクションを含める, Include UOM,UOMを含める, diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv index d2003c004e..2740d7fc8a 100644 --- a/erpnext/translations/km.csv +++ b/erpnext/translations/km.csv @@ -1153,7 +1153,7 @@ In Value,នៅក្នុងតម្លៃ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",ក្នុងករណីមានកម្មវិធីពហុលំដាប់អតិថិជននឹងត្រូវបានចាត់តាំងដោយខ្លួនឯងទៅថ្នាក់ដែលពាក់ព័ន្ធដោយចំណាយរបស់ពួកគេ, Inactive,អសកម្ម, Incentives,ការលើកទឹកចិត្ត, -Include Default Book Entries,រួមបញ្ចូលធាតុសៀវភៅលំនាំដើម។, +Include Default FB Entries,រួមបញ្ចូលធាតុសៀវភៅលំនាំដើម។, Include Exploded Items,រួមបញ្ចូលធាតុផ្ទុះ, Include POS Transactions,បញ្ចូលប្រតិបត្តិការ POS, Include UOM,រួមបញ្ចូល UOM, diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv index 72066711ca..8b271682eb 100644 --- a/erpnext/translations/kn.csv +++ b/erpnext/translations/kn.csv @@ -1153,7 +1153,7 @@ In Value,ಮೌಲ್ಯ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","ಮಲ್ಟಿ-ಟೈರ್ ಪ್ರೋಗ್ರಾಂನ ಸಂದರ್ಭದಲ್ಲಿ, ಗ್ರಾಹಕರು ತಮ್ಮ ಖರ್ಚುಗೆ ಅನುಗುಣವಾಗಿ ಆಯಾ ಶ್ರೇಣಿಗೆ ಸ್ವಯಂ ನಿಯೋಜಿಸಲಾಗುವುದು", Inactive,ನಿಷ್ಕ್ರಿಯವಾಗಿದೆ, Incentives,ಪ್ರೋತ್ಸಾಹ, -Include Default Book Entries,ಡೀಫಾಲ್ಟ್ ಪುಸ್ತಕ ನಮೂದುಗಳನ್ನು ಸೇರಿಸಿ, +Include Default FB Entries,ಡೀಫಾಲ್ಟ್ ಪುಸ್ತಕ ನಮೂದುಗಳನ್ನು ಸೇರಿಸಿ, Include Exploded Items,ಸ್ಫೋಟಗೊಂಡ ವಸ್ತುಗಳನ್ನು ಸೇರಿಸಿ, Include POS Transactions,ಪಿಒಎಸ್ ಟ್ರಾನ್ಸಾಕ್ಷನ್ಸ್ ಸೇರಿಸಿ, Include UOM,UOM ಸೇರಿಸಿ, diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv index 99119256c1..edd3f2731f 100644 --- a/erpnext/translations/ko.csv +++ b/erpnext/translations/ko.csv @@ -1153,7 +1153,7 @@ In Value,값에서, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",다중 계층 프로그램의 경우 고객은 지출 한대로 해당 계층에 자동으로 할당됩니다., Inactive,비활성, Incentives,장려책, -Include Default Book Entries,기본 도서 항목 포함, +Include Default FB Entries,기본 도서 항목 포함, Include Exploded Items,분해 된 항목 포함, Include POS Transactions,POS 트랜잭션 포함, Include UOM,UOM 포함, diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv index 8fec05993d..e18ce45132 100644 --- a/erpnext/translations/ku.csv +++ b/erpnext/translations/ku.csv @@ -1153,7 +1153,7 @@ In Value,di Nirx, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Di rewşê de bernameya pir-tier, Ewrûpa dê ji hêla xercê xwe ve girêdayî xerîb be", Inactive,Bêkar, Incentives,aborîve, -Include Default Book Entries,Navnîşanên Pirtûka Pêvek Bawer bikin, +Include Default FB Entries,Navnîşanên Pirtûka Pêvek Bawer bikin, Include Exploded Items,Included Dead Items, Include POS Transactions,Têkiliyên POSê de, Include UOM,UOM, diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv index 0831788651..46acd22939 100644 --- a/erpnext/translations/lo.csv +++ b/erpnext/translations/lo.csv @@ -1153,7 +1153,7 @@ In Value,ໃນມູນຄ່າ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","ໃນກໍລະນີຂອງໂຄງການຫຼາຍຂັ້ນ, ລູກຄ້າຈະໄດ້ຮັບການມອບຫມາຍໂດຍອັດຕະໂນມັດໃຫ້ກັບຂັ້ນຕອນທີ່ກ່ຽວຂ້ອງຕາມການໃຊ້ຈ່າຍຂອງເຂົາເຈົ້າ", Inactive,Inactive, Incentives,ສິ່ງຈູງໃຈ, -Include Default Book Entries,ລວມທັງການອອກສຽງປື້ມແບບເລີ່ມຕົ້ນ, +Include Default FB Entries,ລວມທັງການອອກສຽງປື້ມແບບເລີ່ມຕົ້ນ, Include Exploded Items,ລວມເອົາສິ່ງທີ່ເກີດຂື້ນ, Include POS Transactions,ລວມທຸລະກໍາ POS, Include UOM,ລວມ UOM, diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv index 82152754e8..292c9d88e2 100644 --- a/erpnext/translations/lt.csv +++ b/erpnext/translations/lt.csv @@ -1153,7 +1153,7 @@ In Value,vertės, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Kalbant apie daugiapakopę programą, klientai bus automatiškai priskirti atitinkamam lygmeniui, atsižvelgiant į jų išleidimą", Inactive,Neaktyvus, Incentives,Paskatos, -Include Default Book Entries,Įtraukite numatytuosius knygų įrašus, +Include Default FB Entries,Įtraukite numatytuosius knygų įrašus, Include Exploded Items,Įtraukti sprogus elementus, Include POS Transactions,Įtraukti POS operacijas, Include UOM,Įtraukti UOM, diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv index 8c4526ca52..52641b25af 100644 --- a/erpnext/translations/lv.csv +++ b/erpnext/translations/lv.csv @@ -1153,7 +1153,7 @@ In Value,Vērtībā, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Daudzpakāpju programmas gadījumā Klienti tiks automātiski piešķirti attiecīgajam līmenim, salīdzinot ar viņu iztērēto", Inactive,Neaktīvs, Incentives,Stimuli, -Include Default Book Entries,Iekļaujiet noklusējuma grāmatas ierakstus, +Include Default FB Entries,Iekļaujiet noklusējuma grāmatas ierakstus, Include Exploded Items,Iekļaut izpūstas preces, Include POS Transactions,Iekļaut POS darījumus, Include UOM,Iekļaut UOM, diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv index a622524bf1..0a290198cc 100644 --- a/erpnext/translations/mk.csv +++ b/erpnext/translations/mk.csv @@ -1153,7 +1153,7 @@ In Value,Во вредност, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Во случај на повеќеслојна програма, корисниците ќе бидат автоматски доделени на засегнатите нивоа, како на нивните потрошени", Inactive,Неактивен, Incentives,Стимулации, -Include Default Book Entries,Вклучете стандардни записи за книги, +Include Default FB Entries,Вклучете стандардни записи за книги, Include Exploded Items,Вклучи експлодирани елементи, Include POS Transactions,Вклучете POS-трансакции, Include UOM,Вклучете UOM, diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv index 777d5c64ff..04af8ab1ad 100644 --- a/erpnext/translations/ml.csv +++ b/erpnext/translations/ml.csv @@ -1153,7 +1153,7 @@ In Value,മൂല്യത്തിൽ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","മൾട്ടി-ടയർ പരിപാടിയുടെ കാര്യത്തിൽ, കസ്റ്റമർമാർ ചെലവഴിച്ച തുക പ്രകാരം ബന്ധപ്പെട്ട ടീമിൽ ഓട്ടോ നിർണ്ണയിക്കും", Inactive,നിഷ്ക്രിയം, Incentives,ഇൻസെന്റീവ്സ്, -Include Default Book Entries,സ്ഥിരസ്ഥിതി പുസ്തക എൻ‌ട്രികൾ‌ ഉൾ‌പ്പെടുത്തുക, +Include Default FB Entries,സ്ഥിരസ്ഥിതി പുസ്തക എൻ‌ട്രികൾ‌ ഉൾ‌പ്പെടുത്തുക, Include Exploded Items,എക്സ്പ്ലോഡഡ് ഇനങ്ങൾ ഉൾപ്പെടുത്തുക, Include POS Transactions,POS ഇടപാടുകൾ ഉൾപ്പെടുത്തുക, Include UOM,UOM ഉൾപ്പെടുത്തുക, diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv index 624f1ab481..785ab65c54 100644 --- a/erpnext/translations/mr.csv +++ b/erpnext/translations/mr.csv @@ -1153,7 +1153,7 @@ In Value,मूल्य, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",बहु-स्तरीय कार्यक्रमाच्या बाबतीत ग्राहक त्यांच्या खर्चानुसार संबंधित टायरला स्वयंचलितरित्या नियुक्त केले जातील, Inactive,निष्क्रिय, Incentives,प्रोत्साहन, -Include Default Book Entries,डीफॉल्ट पुस्तक नोंदी समाविष्ट करा, +Include Default FB Entries,डीफॉल्ट पुस्तक नोंदी समाविष्ट करा, Include Exploded Items,विस्फोट केलेल्या वस्तू समाविष्ट करा, Include POS Transactions,पीओएस व्यवहार समाविष्ट करा, Include UOM,यूओएम समाविष्ट करा, diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv index 75e150a88d..db20d3c054 100644 --- a/erpnext/translations/ms.csv +++ b/erpnext/translations/ms.csv @@ -1153,7 +1153,7 @@ In Value,Dalam Nilai, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Dalam hal program multi-tier, Pelanggan akan ditugaskan secara automatik ke peringkat yang bersangkutan seperti yang dibelanjakannya", Inactive,Tidak aktif, Incentives,Insentif, -Include Default Book Entries,Sertakan Penyertaan Buku Lalai, +Include Default FB Entries,Sertakan Penyertaan Buku Lalai, Include Exploded Items,Termasuk Item Meletup, Include POS Transactions,Termasuk Transaksi POS, Include UOM,Termasuk UOM, diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv index 36cd8740d7..f4b8676fee 100644 --- a/erpnext/translations/my.csv +++ b/erpnext/translations/my.csv @@ -1153,7 +1153,7 @@ In Value,Value တစ်ခုအတွက်, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Multi-tier အစီအစဉ်၏အမှု၌, Customer များအလိုအလျောက်သူတို့ရဲ့သုံးစွဲနှုန်းအတိုင်းစိုးရိမ်ပူပန်ဆင့်မှတာဝန်ပေးအပ်ပါလိမ့်မည်", Inactive,မလှုပ်ရှားတတ်သော, Incentives,မက်လုံးတွေပေးပြီး, -Include Default Book Entries,ပုံမှန်စာအုပ် Entries Include, +Include Default FB Entries,ပုံမှန်စာအုပ် Entries Include, Include Exploded Items,ပေါက်ကွဲပစ္စည်းများ Include, Include POS Transactions,POS အရောင်းအဝယ် Include, Include UOM,UOM Include, diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index 5859833861..1778c8044a 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -1153,7 +1153,7 @@ In Value,in Value, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",In het geval van een meerlagig programma worden klanten automatisch toegewezen aan de betreffende laag op basis van hun bestede tijd, Inactive,Inactief, Incentives,Incentives, -Include Default Book Entries,Standaard boekvermeldingen opnemen, +Include Default FB Entries,Standaard boekvermeldingen opnemen, Include Exploded Items,Exploded Items opnemen, Include POS Transactions,POS-transacties opnemen, Include UOM,Inclusief UOM, diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv index a3236ac084..542217afe7 100644 --- a/erpnext/translations/no.csv +++ b/erpnext/translations/no.csv @@ -1153,7 +1153,7 @@ In Value,I verdi, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Når det gjelder flerlagsprogram, vil kundene automatisk bli tilordnet den aktuelle delen som brukt", Inactive,inaktiv, Incentives,Motivasjon, -Include Default Book Entries,Inkluder standardbokoppføringer, +Include Default FB Entries,Inkluder standardbokoppføringer, Include Exploded Items,Inkluder eksploderte elementer, Include POS Transactions,Inkluder POS-transaksjoner, Include UOM,Inkluder UOM, diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv index df41e39862..247d0bae11 100644 --- a/erpnext/translations/pl.csv +++ b/erpnext/translations/pl.csv @@ -1151,7 +1151,7 @@ In Stock: ,W magazynie:, In Value,w polu Wartość, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","W przypadku programu wielowarstwowego Klienci zostaną automatycznie przypisani do danego poziomu, zgodnie z wydatkami", Inactive,Nieaktywny, -Include Default Book Entries,Dołącz domyślne wpisy książki, +Include Default FB Entries,Dołącz domyślne wpisy książki, Include Exploded Items,Dołącz rozstrzelone przedmioty, Include POS Transactions,Uwzględnij transakcje POS, Include UOM,Dołącz UOM, diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv index 5a0b2a50cb..09d4df31ff 100644 --- a/erpnext/translations/ps.csv +++ b/erpnext/translations/ps.csv @@ -1153,7 +1153,7 @@ In Value,په ارزښت, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",د څو اړخیز پروګرام په صورت کې، پیرودونکي به د خپل مصرف په اساس اړوند ټیټ ته ګمارل کیږي, Inactive,غیر فعال, Incentives,هڅوونکي, -Include Default Book Entries,د ډیفالټ کتاب ننوتل شامل کړئ, +Include Default FB Entries,د ډیفالټ کتاب ننوتل شامل کړئ, Include Exploded Items,چاودیدونکي توکي شامل کړئ, Include POS Transactions,د POS تعاملات شامل کړئ, Include UOM,UOM شامل کړئ, diff --git a/erpnext/translations/pt-BR.csv b/erpnext/translations/pt-BR.csv index bc5b616080..92845b0a40 100644 --- a/erpnext/translations/pt-BR.csv +++ b/erpnext/translations/pt-BR.csv @@ -1153,7 +1153,7 @@ In Value,Valor Entrada, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","No caso do programa multicamadas, os Clientes serão atribuídos automaticamente ao nível em questão de acordo com o gasto", Inactive,Inativo, Incentives,Incentivos, -Include Default Book Entries,Incluir Entradas de Livro Padrão, +Include Default FB Entries,Incluir Entradas de Livro Padrão, Include Exploded Items,Incluir Itens Explodidos, Include POS Transactions,Incluir Transações PDV, Include UOM,Incluir UDM, diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv index e6846c6a37..58cf6c8316 100644 --- a/erpnext/translations/pt.csv +++ b/erpnext/translations/pt.csv @@ -1153,7 +1153,7 @@ In Value,No Valor, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","No caso do programa multicamadas, os Clientes serão atribuídos automaticamente ao nível em questão de acordo com o gasto", Inactive,Inativo, Incentives,Incentivos, -Include Default Book Entries,Incluir entradas de livro padrão, +Include Default FB Entries,Incluir entradas de livro padrão, Include Exploded Items,Incluir itens explodidos, Include POS Transactions,Incluir transações POS, Include UOM,Incluir UOM, diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv index ac7e598e2d..935b1e66fd 100644 --- a/erpnext/translations/ro.csv +++ b/erpnext/translations/ro.csv @@ -1153,7 +1153,7 @@ In Value,În valoare, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","În cazul unui program cu mai multe niveluri, Clienții vor fi automat alocați nivelului respectiv în funcție de cheltuielile efectuate", Inactive,Inactiv, Incentives,stimulente, -Include Default Book Entries,Includeți intrări implicite în cărți, +Include Default FB Entries,Includeți intrări implicite în cărți, Include Exploded Items,Includeți articole explodate, Include POS Transactions,Includeți tranzacțiile POS, Include UOM,Includeți UOM, diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 52c29982f3..2f6f361b10 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -1151,7 +1151,7 @@ In Value,В цене, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",В случае многоуровневой программы Клиенты будут автоматически назначены соответствующему уровню в соответствии с затраченными, Inactive,Неактивный, Incentives,Стимулирование, -Include Default Book Entries,Включить записи в книгу по умолчанию, +Include Default FB Entries,Включить записи в книгу по умолчанию, Include Exploded Items,Включить раздробленные элементы, Include POS Transactions,Включить POS-транзакции, Include UOM,Включить UOM, diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv index f035d57985..59362a1e29 100644 --- a/erpnext/translations/rw.csv +++ b/erpnext/translations/rw.csv @@ -1153,7 +1153,7 @@ In Value,Agaciro, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Kubijyanye na gahunda yo mu byiciro byinshi, Abakiriya bazahabwa imodoka bashinzwe urwego bireba nkuko bakoresheje", Inactive,Kudakora, Incentives,Inkunga, -Include Default Book Entries,Shyiramo Ibisanzwe Byanditswe, +Include Default FB Entries,Shyiramo Ibisanzwe Byanditswe, Include Exploded Items,Shyiramo Ibintu Biturika, Include POS Transactions,Shyiramo ibikorwa bya POS, Include UOM,Shyiramo UOM, diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv index 4047263492..dd2acc45a2 100644 --- a/erpnext/translations/si.csv +++ b/erpnext/translations/si.csv @@ -1153,7 +1153,7 @@ In Value,අගය දී, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","බහු ස්ථරයේ වැඩසටහනක දී, පාරිභෝගිකයින් විසින් වැය කරනු ලබන පරිදි පාරිභෝගිකයින්ට අදාල ස්ථානයට ස්වයංක්රීයව පවරනු ලැබේ", Inactive,අක්රියයි, Incentives,සහන, -Include Default Book Entries,පෙරනිමි පොත් ඇතුළත් කිරීම් ඇතුළත් කරන්න, +Include Default FB Entries,පෙරනිමි පොත් ඇතුළත් කිරීම් ඇතුළත් කරන්න, Include Exploded Items,පුපුරණ ද්රව්ය අඩංගු කරන්න, Include POS Transactions,POS ගනුදෙනු, Include UOM,UOM ඇතුළත් කරන්න, diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv index 98e1663f1c..025c8b788f 100644 --- a/erpnext/translations/sk.csv +++ b/erpnext/translations/sk.csv @@ -1153,7 +1153,7 @@ In Value,v Hodnota, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",V prípade viacvrstvového programu budú zákazníci automaticky priradení príslušnému vrstvu podľa ich vynaložených prostriedkov, Inactive,neaktívne, Incentives,Pobídky, -Include Default Book Entries,Zahrnúť predvolené položky knihy, +Include Default FB Entries,Zahrnúť predvolené položky knihy, Include Exploded Items,Zahrňte explodované položky, Include POS Transactions,Zahrňte POS transakcie, Include UOM,Zahrňte UOM, diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv index 5380714bdc..86b5e58129 100644 --- a/erpnext/translations/sl.csv +++ b/erpnext/translations/sl.csv @@ -1153,7 +1153,7 @@ In Value,V vrednosti, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",V primeru večstopenjskega programa bodo stranke samodejno dodeljene zadevni stopnji glede na porabljene, Inactive,Neaktivno, Incentives,Spodbude, -Include Default Book Entries,Vključi privzete vnose v knjige, +Include Default FB Entries,Vključi privzete vnose v knjige, Include Exploded Items,Vključi eksplodirane elemente, Include POS Transactions,Vključite POS transakcije, Include UOM,Vključi UOM, diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv index 2a893d272b..3cfa4297df 100644 --- a/erpnext/translations/sq.csv +++ b/erpnext/translations/sq.csv @@ -1153,7 +1153,7 @@ In Value,Në Vlera, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Në rastin e programit multi-shtresor, Konsumatorët do të caktohen automatikisht në nivelin përkatës sipas shpenzimeve të tyre", Inactive,joaktiv, Incentives,Nxitjet, -Include Default Book Entries,Përfshini hyrje të librave të paracaktuar, +Include Default FB Entries,Përfshini hyrje të librave të paracaktuar, Include Exploded Items,Përfshirja e artikujve të eksploduar, Include POS Transactions,Përfshi transaksione POS, Include UOM,Përfshi UOM, diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv index c1e5eb0eea..621772f39f 100644 --- a/erpnext/translations/sr.csv +++ b/erpnext/translations/sr.csv @@ -1153,7 +1153,7 @@ In Value,У вредности, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","У случају мулти-тиер програма, Корисници ће аутоматски бити додијељени за одређени ниво према њиховом потрошеном", Inactive,Неактиван, Incentives,Подстицаји, -Include Default Book Entries,Укључивање заданих уноса у књиге, +Include Default FB Entries,Укључивање заданих уноса у књиге, Include Exploded Items,Укључите експлодиране ставке, Include POS Transactions,Укључите ПОС трансакције, Include UOM,Укључите УОМ, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index 8b4ab068eb..4fef88b7f4 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -1153,7 +1153,7 @@ In Value,Värde, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",När det gäller program med flera nivåer kommer kunderna automatiskt att tilldelas den aktuella tiern enligt deras tillbringade, Inactive,Inaktiv, Incentives,Sporen, -Include Default Book Entries,Inkludera standardbokposter, +Include Default FB Entries,Inkludera standardbokposter, Include Exploded Items,Inkludera explosiva artiklar, Include POS Transactions,Inkludera POS-transaktioner, Include UOM,Inkludera UOM, diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv index fa2287c3bb..3b4d8aee64 100644 --- a/erpnext/translations/sw.csv +++ b/erpnext/translations/sw.csv @@ -1153,7 +1153,7 @@ In Value,Kwa Thamani, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Katika kesi ya mpango wa mipango mbalimbali, Wateja watapatiwa auto kwa tier husika kama kwa matumizi yao", Inactive,Haikufanya kazi, Incentives,Vidokezo, -Include Default Book Entries,Jumuisha Ingizo Mbadala za Kitabu, +Include Default FB Entries,Jumuisha Ingizo Mbadala za Kitabu, Include Exploded Items,Jumuisha Vipengee Vipengee, Include POS Transactions,Jumuisha Shughuli za POS, Include UOM,Jumuisha UOM, diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv index 6eaae34a6e..f40e512427 100644 --- a/erpnext/translations/ta.csv +++ b/erpnext/translations/ta.csv @@ -1153,7 +1153,7 @@ In Value,மதிப்பு, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","பல அடுக்கு திட்டத்தின் விஷயத்தில், வாடிக்கையாளர்கள் தங்கள் செலவினங்களின்படி சம்பந்தப்பட்ட அடுக்குக்கு கார் ஒதுக்கப்படுவார்கள்", Inactive,செயல்படா, Incentives,செயல் தூண்டுதல், -Include Default Book Entries,இயல்புநிலை புத்தக உள்ளீடுகளைச் சேர்க்கவும், +Include Default FB Entries,இயல்புநிலை புத்தக உள்ளீடுகளைச் சேர்க்கவும், Include Exploded Items,வெடித்துள்ள பொருட்கள் அடங்கும், Include POS Transactions,POS பரிமாற்றங்களைச் சேர்க்கவும், Include UOM,UOM ஐ சேர்க்கவும், diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv index d3f739af8b..22fd7d7d1d 100644 --- a/erpnext/translations/te.csv +++ b/erpnext/translations/te.csv @@ -1153,7 +1153,7 @@ In Value,విలువ, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","మల్టీ-టైర్ ప్రోగ్రామ్ విషయంలో, కస్టమర్లు వారి గడువు ప్రకారం, సంబంధిత స్థాయికి కేటాయించబడతారు", Inactive,క్రియారహిత, Incentives,ఇన్సెంటివ్స్, -Include Default Book Entries,డిఫాల్ట్ బుక్ ఎంట్రీలను చేర్చండి, +Include Default FB Entries,డిఫాల్ట్ బుక్ ఎంట్రీలను చేర్చండి, Include Exploded Items,ఎక్స్ప్లోడ్ ఐటెమ్లను చేర్చండి, Include POS Transactions,POS లావాదేవీలను చేర్చండి, Include UOM,UOM ని చేర్చండి, diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv index a065595898..5dfb93c585 100644 --- a/erpnext/translations/th.csv +++ b/erpnext/translations/th.csv @@ -1153,7 +1153,7 @@ In Value,ในราคา, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",ในกรณีของโปรแกรมแบบหลายชั้นลูกค้าจะได้รับการกำหนดให้โดยอัตโนมัติตามระดับที่เกี่ยวข้องตามการใช้จ่าย, Inactive,เฉื่อยชา, Incentives,แรงจูงใจ, -Include Default Book Entries,รวมรายการหนังสือเริ่มต้น, +Include Default FB Entries,รวมรายการหนังสือเริ่มต้น, Include Exploded Items,รวมรายการที่ระเบิดแล้ว, Include POS Transactions,รวมธุรกรรม POS, Include UOM,รวม UOM, diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index 9e916f0315..82d28240c1 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -1153,7 +1153,7 @@ In Value,Giriş Maliyeti, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Çok katmanlı program söz konusu olduğunda, Müşteriler harcanan esasa göre ilgili kademeye otomatik olarak atanacaktır.", Inactive,etkisiz, Incentives,Teşvikler, -Include Default Book Entries,Varsayılan Defter Girişlerini Dahil et, +Include Default FB Entries,Varsayılan Defter Girişlerini Dahil et, Include Exploded Items,Patlatılmış Öğeleri Dahil et, Include POS Transactions,POS İşlemlerini Dahil et, Include UOM,Birimi Dahil et, diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv index 8d1fb04fdb..f77c6da35e 100644 --- a/erpnext/translations/uk.csv +++ b/erpnext/translations/uk.csv @@ -1153,7 +1153,7 @@ In Value,У Сумі, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","У випадку багаторівневої програми, Клієнти будуть автоматично призначені для відповідного рівня відповідно до витрачених ними витрат", Inactive,Неактивний, Incentives,Стимули, -Include Default Book Entries,Включити записи за замовчуванням, +Include Default FB Entries,Включити записи за замовчуванням, Include Exploded Items,Включити вибухнуті елементи, Include POS Transactions,Включити POS-транзакції, Include UOM,Включити UOM, diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv index 649c1c7759..4dc872be5d 100644 --- a/erpnext/translations/ur.csv +++ b/erpnext/translations/ur.csv @@ -1153,7 +1153,7 @@ In Value,قدر میں, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",کثیر درجے کے پروگرام کے معاملے میں، صارفین اپنے اخراجات کے مطابق متعلقہ درجے کو خود کار طریقے سے تفویض کریں گے, Inactive,غیر فعال, Incentives,ترغیبات, -Include Default Book Entries,ڈیفالٹ کتاب اندراجات شامل کریں۔, +Include Default FB Entries,ڈیفالٹ کتاب اندراجات شامل کریں۔, Include Exploded Items,دھماکہ خیز اشیاء شامل کریں, Include POS Transactions,پی او ایس کے لین دین میں شامل کریں, Include UOM,UOM شامل کریں, diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv index 5ca51ccb0f..c09aa895e9 100644 --- a/erpnext/translations/uz.csv +++ b/erpnext/translations/uz.csv @@ -1153,7 +1153,7 @@ In Value,Qiymatida, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",Ko'p qatlamli dasturda mijozlar o'zlari sarflagan xarajatlariga muvofiq tegishli darajaga avtomatik tarzda topshiriladi, Inactive,Faol emas, Incentives,Rag'batlantirish, -Include Default Book Entries,Odatiy kitob yozuvlarini qo'shing, +Include Default FB Entries,Odatiy kitob yozuvlarini qo'shing, Include Exploded Items,Portlatilgan narsalarni qo'shish, Include POS Transactions,Qalin operatsiyalarni qo'shish, Include UOM,UOM ni qo'shing, diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv index 7a005fa414..eb251a5978 100644 --- a/erpnext/translations/vi.csv +++ b/erpnext/translations/vi.csv @@ -1153,7 +1153,7 @@ In Value,Trong giá trị, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent","Trong trường hợp chương trình nhiều tầng, Khách hàng sẽ được tự động chỉ định cho cấp có liên quan theo mức chi tiêu của họ", Inactive,Không hoạt động, Incentives,Ưu đãi, -Include Default Book Entries,Bao gồm các mục sách mặc định, +Include Default FB Entries,Bao gồm các mục sách mặc định, Include Exploded Items,Bao gồm các mục đã Phát hiện, Include POS Transactions,Bao gồm giao dịch POS, Include UOM,Bao gồm UOM, diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index cf89dc6852..08f8d33578 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -1153,7 +1153,7 @@ In Value,金额, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",在多层程序的情况下,客户将根据其花费自动分配到相关层, Inactive,非活动的, Incentives,激励政策, -Include Default Book Entries,包括默认工作簿条目, +Include Default FB Entries,包括默认工作簿条目, Include Exploded Items,包含爆炸物料, Include POS Transactions,包括POS交易, Include UOM,包括基本计量单位, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index 8ecbaaa9c5..dd683c5a27 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -1166,7 +1166,7 @@ In Value,在數值, "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent",在多層程序的情況下,客戶將根據其花費自動分配到相關層, Inactive,待用, Incentives,獎勵, -Include Default Book Entries,包括默認工作簿條目, +Include Default FB Entries,包括默認工作簿條目, Include Exploded Items,包含爆炸物品, Include UOM,包括UOM, Included in Gross Profit,包含在毛利潤中, diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 5e57b31793..df21b61139 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -3,6 +3,7 @@ from datetime import date, datetime import frappe from frappe import _ +from frappe.utils import get_link_to_form, today @frappe.whitelist() @@ -28,6 +29,51 @@ def transaction_processing(data, from_doctype, to_doctype): job(deserialized_data, from_doctype, to_doctype) +@frappe.whitelist() +def retry(date: str | None = None): + if not date: + date = today() + + if date: + failed_docs = frappe.db.get_all( + "Bulk Transaction Log Detail", + filters={"date": date, "transaction_status": "Failed", "retried": 0}, + fields=["name", "transaction_name", "from_doctype", "to_doctype"], + ) + if not failed_docs: + frappe.msgprint(_("There are no Failed transactions")) + else: + job = frappe.enqueue( + retry_failed_transactions, + failed_docs=failed_docs, + ) + frappe.msgprint( + _("Job: {0} has been triggered for processing failed transactions").format( + get_link_to_form("RQ Job", job.id) + ) + ) + + +def retry_failed_transactions(failed_docs: list | None): + if failed_docs: + for log in failed_docs: + try: + frappe.db.savepoint("before_creation_state") + task(log.transaction_name, log.from_doctype, log.to_doctype) + except Exception as e: + frappe.db.rollback(save_point="before_creation_state") + update_log(log.name, "Failed", 1, str(frappe.get_traceback())) + else: + update_log(log.name, "Success", 1) + + +def update_log(log_name, status, retried, err=None): + frappe.db.set_value("Bulk Transaction Log Detail", log_name, "transaction_status", status) + frappe.db.set_value("Bulk Transaction Log Detail", log_name, "retried", retried) + if err: + frappe.db.set_value("Bulk Transaction Log Detail", log_name, "error_description", err) + + def job(deserialized_data, from_doctype, to_doctype): fail_count = 0 for d in deserialized_data: @@ -38,7 +84,7 @@ def job(deserialized_data, from_doctype, to_doctype): except Exception as e: frappe.db.rollback(save_point="before_creation_state") fail_count += 1 - update_logger( + create_log( doc_name, str(frappe.get_traceback()), from_doctype, @@ -47,7 +93,7 @@ def job(deserialized_data, from_doctype, to_doctype): log_date=str(date.today()), ) else: - update_logger( + create_log( doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()) ) @@ -98,6 +144,7 @@ def task(doc_name, from_doctype, to_doctype): }, "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, } + frappe.flags.bulk_transaction = True if to_doctype in ["Payment Entry"]: obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) else: @@ -106,47 +153,21 @@ def task(doc_name, from_doctype, to_doctype): obj.flags.ignore_validate = True obj.set_title_field() obj.insert(ignore_mandatory=True) + del frappe.flags.bulk_transaction -def check_logger_doc_exists(log_date): - return frappe.db.exists("Bulk Transaction Log", log_date) - - -def get_logger_doc(log_date): - return frappe.get_doc("Bulk Transaction Log", log_date) - - -def create_logger_doc(): - log_doc = frappe.new_doc("Bulk Transaction Log") - log_doc.set_new_name(set_name=str(date.today())) - log_doc.log_date = date.today() - - return log_doc - - -def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted): - row = log_doc.append("logger_data", {}) - row.transaction_name = doc_name - row.date = date.today() +def create_log(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0): + transaction_log = frappe.new_doc("Bulk Transaction Log Detail") + transaction_log.transaction_name = doc_name + transaction_log.date = today() now = datetime.now() - row.time = now.strftime("%H:%M:%S") - row.transaction_status = status - row.error_description = str(error) - row.from_doctype = from_doctype - row.to_doctype = to_doctype - row.retried = restarted - - -def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0): - if not check_logger_doc_exists(log_date): - log_doc = create_logger_doc() - append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted) - log_doc.insert() - else: - log_doc = get_logger_doc(log_date) - if record_exists(log_doc, doc_name, status): - append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted) - log_doc.save() + transaction_log.time = now.strftime("%H:%M:%S") + transaction_log.transaction_status = status + transaction_log.error_description = str(e) + transaction_log.from_doctype = from_doctype + transaction_log.to_doctype = to_doctype + transaction_log.retried = restarted + transaction_log.save() def show_job_status(fail_count, deserialized_data_count, to_doctype): @@ -176,25 +197,3 @@ def show_job_status(fail_count, deserialized_data_count, to_doctype): title="Failed", indicator="red", ) - - -def record_exists(log_doc, doc_name, status): - record = mark_retrired_transaction(log_doc, doc_name) - if record and status == "Failed": - return False - elif record and status == "Success": - return True - else: - return True - - -def mark_retrired_transaction(log_doc, doc_name): - record = 0 - for d in log_doc.get("logger_data"): - if d.transaction_name == doc_name and d.transaction_status == "Failed": - d.retried = 1 - record = record + 1 - - log_doc.save() - - return record diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 7eba35dedd..b083614a5f 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater): "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] ) + stop_actions = [] for ref_dt, ref_dn_field, ref_link_field in ref_details: reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] reference_details = self.get_reference_details(reference_names, ref_dt + " Item") @@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater): if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": if role_allowed_to_override not in frappe.get_roles(): - frappe.throw( + stop_actions.append( _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate ) @@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater): title=_("Warning"), indicator="orange", ) + if stop_actions: + frappe.throw(stop_actions, as_list=True) def get_reference_details(self, reference_names, reference_doctype): return frappe._dict(