diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 3dfbf701060..7361cb2fc1c 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -120,52 +120,66 @@ frappe.ui.form.on("Bank Statement Import", { }, show_import_status(frm) { - let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); - let successful_records = import_log.filter((log) => log.success); - let failed_records = import_log.filter((log) => !log.success); - if (successful_records.length === 0) return; + if (frm.doc.status == "Pending") return; - let message; - if (failed_records.length === 0) { - let message_args = [successful_records.length]; - if (frm.doc.import_type === "Insert New Records") { - message = - successful_records.length > 1 - ? __("Successfully imported {0} records.", message_args) - : __("Successfully imported {0} record.", message_args); - } else { - message = - successful_records.length > 1 - ? __("Successfully updated {0} records.", message_args) - : __("Successfully updated {0} record.", message_args); - } - } else { - let message_args = [successful_records.length, import_log.length]; - if (frm.doc.import_type === "Insert New Records") { - message = - successful_records.length > 1 - ? __( - "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) - : __( - "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); - } else { - message = - successful_records.length > 1 - ? __( - "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) - : __( - "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); - } - } - frm.dashboard.set_headline(message); + frappe.call({ + method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.get_import_status", + args: { + docname: frm.doc.name, + }, + callback: function (r) { + let successful_records = cint(r.message.success); + let failed_records = cint(r.message.failed); + let total_records = cint(r.message.total_records); + + if (!total_records) { + return; + } + + let message; + if (failed_records === 0) { + let message_args = [successful_records]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records > 1 + ? __("Successfully imported {0} records.", message_args) + : __("Successfully imported {0} record.", message_args); + } else { + message = + successful_records > 1 + ? __("Successfully updated {0} records.", message_args) + : __("Successfully updated {0} record.", message_args); + } + } else { + let message_args = [successful_records, total_records]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records > 1 + ? __( + "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } else { + message = + successful_records > 1 + ? __( + "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } + } + + frm.dashboard.set_headline(message); + }, + }); }, show_report_error_button(frm) { @@ -287,7 +301,7 @@ frappe.ui.form.on("Bank Statement Import", { // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', show_import_preview(frm, preview_data) { - let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); + let import_log = preview_data.import_log; if (frm.import_preview && frm.import_preview.doctype === frm.doc.reference_doctype) { frm.import_preview.preview_data = preview_data; @@ -326,6 +340,15 @@ frappe.ui.form.on("Bank Statement Import", { ); }, + export_import_log(frm) { + open_url_post( + "/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_import_log", + { + data_import_name: frm.doc.name, + } + ); + }, + show_import_warnings(frm, preview_data) { let columns = preview_data.columns; let warnings = JSON.parse(frm.doc.template_warnings || "[]"); @@ -401,49 +424,50 @@ frappe.ui.form.on("Bank Statement Import", { frm.trigger("show_import_log"); }, - show_import_log(frm) { - let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); - let logs = import_log; - frm.toggle_display("import_log", false); - frm.toggle_display("import_log_section", logs.length > 0); + render_import_log(frm) { + frappe.call({ + method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.get_import_logs", + args: { + docname: frm.doc.name, + }, + callback: function (r) { + let logs = r.message; - if (logs.length === 0) { - frm.get_field("import_log_preview").$wrapper.empty(); - return; - } + if (logs.length === 0) return; - let rows = logs - .map((log) => { - let html = ""; - if (log.success) { - if (frm.doc.import_type === "Insert New Records") { - html = __("Successfully imported {0}", [ - `${frappe.utils.get_form_link( - frm.doc.reference_doctype, - log.docname, - true - )}`, - ]); - } else { - html = __("Successfully updated {0}", [ - `${frappe.utils.get_form_link( - frm.doc.reference_doctype, - log.docname, - true - )}`, - ]); - } - } else { - let messages = log.messages - .map(JSON.parse) - .map((m) => { - let title = m.title ? `${m.title}` : ""; - let message = m.message ? `
${m.message}
` : ""; - return title + message; - }) - .join(""); - let id = frappe.dom.get_unique_id(); - html = `${messages} + frm.toggle_display("import_log_section", true); + + let rows = logs + .map((log) => { + let html = ""; + if (log.success) { + if (frm.doc.import_type === "Insert New Records") { + html = __("Successfully imported {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); + } else { + html = __("Successfully updated {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ]); + } + } else { + let messages = JSON.parse(log.messages || "[]") + .map((m) => { + let title = m.title ? `${m.title}` : ""; + let message = m.message ? `
${m.message}
` : ""; + return title + message; + }) + .join(""); + let id = frappe.dom.get_unique_id(); + html = `${messages} @@ -452,16 +476,16 @@ frappe.ui.form.on("Bank Statement Import", {
${log.exception}
`; - } - let indicator_color = log.success ? "green" : "red"; - let title = log.success ? __("Success") : __("Failure"); + } + let indicator_color = log.success ? "green" : "red"; + let title = log.success ? __("Success") : __("Failure"); - if (frm.doc.show_failed_logs && log.success) { - return ""; - } + if (frm.doc.show_failed_logs && log.success) { + return ""; + } - return ` - ${log.row_indexes.join(", ")} + return ` + ${JSON.parse(log.row_indexes).join(", ")}
${title}
@@ -469,16 +493,16 @@ frappe.ui.form.on("Bank Statement Import", { ${html} `; - }) - .join(""); + }) + .join(""); - if (!rows && frm.doc.show_failed_logs) { - rows = ` + if (!rows && frm.doc.show_failed_logs) { + rows = ` ${__("No failed logs")} `; - } + } - frm.get_field("import_log_preview").$wrapper.html(` + frm.get_field("import_log_preview").$wrapper.html(` @@ -488,5 +512,34 @@ frappe.ui.form.on("Bank Statement Import", { ${rows}
${__("Row Number")}
`); + }, + }); + }, + + show_import_log(frm) { + frm.toggle_display("import_log_section", false); + + if (frm.is_new() || frm.import_in_progress) { + return; + } + + frappe.call({ + method: "frappe.client.get_count", + args: { + doctype: "Data Import Log", + filters: { + data_import: frm.doc.name, + }, + }, + callback: function (r) { + let count = r.message; + if (count < 5000) { + frm.trigger("render_import_log"); + } else { + frm.toggle_display("import_log_section", false); + frm.add_custom_button(__("Export Import Log"), () => frm.trigger("export_import_log")); + } + }, + }); }, }); diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json index eede3bdc6d1..3210457c645 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -11,6 +11,8 @@ "bank_account", "bank", "column_break_4", + "custom_delimiters", + "delimiter_options", "google_sheets_url", "refresh_google_sheet", "html_5", @@ -24,7 +26,6 @@ "section_import_preview", "import_preview", "import_log_section", - "statement_import_log", "show_failed_logs", "import_log_preview", "reference_doctype", @@ -194,15 +195,23 @@ "fieldtype": "Column Break" }, { - "fieldname": "statement_import_log", - "fieldtype": "Code", - "label": "Statement Import Log", - "options": "JSON" + "default": "0", + "fieldname": "custom_delimiters", + "fieldtype": "Check", + "label": "Custom delimiters" + }, + { + "default": ",;\\t|", + "depends_on": "custom_delimiters", + "description": "If your CSV uses a different delimiter, add that character here, ensuring no spaces or additional characters are included.", + "fieldname": "delimiter_options", + "fieldtype": "Data", + "label": "Delimiter options" } ], "hide_toolbar": 1, "links": [], - "modified": "2022-09-07 11:11:40.293317", + "modified": "2024-06-25 17:32:07.658250", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Statement Import", diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 0d19e212cba..fd18819187e 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -31,13 +31,14 @@ class BankStatementImport(DataImport): bank: DF.Link | None bank_account: DF.Link company: DF.Link + custom_delimiters: DF.Check + delimiter_options: DF.Data | None google_sheets_url: DF.Data | None import_file: DF.Attach | None import_type: DF.Literal["", "Insert New Records", "Update Existing Records"] mute_emails: DF.Check reference_doctype: DF.Link show_failed_logs: DF.Check - statement_import_log: DF.Code | None status: DF.Literal["Pending", "Success", "Partial Success", "Error"] submit_after_import: DF.Check template_options: DF.Code | None @@ -120,6 +121,11 @@ def download_errored_template(data_import_name): data_import.export_errored_rows() +@frappe.whitelist() +def download_import_log(data_import_name): + return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log() + + def parse_data_from_template(raw_data): data = [] @@ -241,6 +247,47 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): return True +@frappe.whitelist() +def get_import_status(docname): + import_status = {} + + data_import = frappe.get_doc("Bank Statement Import", docname) + import_status["status"] = data_import.status + + logs = frappe.get_all( + "Data Import Log", + fields=["count(*) as count", "success"], + filters={"data_import": docname}, + group_by="success", + ) + + total_payload_count = 0 + + for log in logs: + total_payload_count += log.get("count", 0) + if log.get("success"): + import_status["success"] = log.get("count") + else: + import_status["failed"] = log.get("count") + + import_status["total_records"] = total_payload_count + + return import_status + + +@frappe.whitelist() +def get_import_logs(docname: str): + frappe.has_permission("Bank Statement Import") + + return frappe.get_all( + "Data Import Log", + fields=["success", "docname", "messages", "exception", "row_indexes"], + filters={"data_import": docname}, + limit_page_length=5000, + order_by="log_index", + ) + + @frappe.whitelist() def upload_bank_statement(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index fb3500e2c58..1103cd09795 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -82,7 +82,6 @@ class PaymentEntry(AccountsController): self.set_exchange_rate() self.validate_mandatory() self.validate_reference_documents() - self.set_tax_withholding() self.set_amounts() self.validate_amounts() self.apply_taxes() @@ -96,6 +95,7 @@ class PaymentEntry(AccountsController): self.validate_allocated_amount() self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() + self.set_tax_withholding() self.set_status() self.set_total_in_words() @@ -756,9 +756,7 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - order_amount = self.get_order_net_total() - - net_total = flt(order_amount) + flt(self.unallocated_amount) + net_total = self.calculate_tax_withholding_net_total() # Adding args as purchase invoice to get TDS amount args = frappe._dict( @@ -802,7 +800,26 @@ class PaymentEntry(AccountsController): for d in to_remove: self.remove(d) - def get_order_net_total(self): + def calculate_tax_withholding_net_total(self): + net_total = 0 + order_details = self.get_order_wise_tax_withholding_net_total() + + for d in self.references: + tax_withholding_net_total = order_details.get(d.reference_name) + if not tax_withholding_net_total: + continue + + net_taxable_outstanding = max( + 0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total) + ) + + net_total += min(net_taxable_outstanding, d.allocated_amount) + + net_total += self.unallocated_amount + + return net_total + + def get_order_wise_tax_withholding_net_total(self): if self.party_type == "Supplier": doctype = "Purchase Order" else: @@ -810,12 +827,15 @@ class PaymentEntry(AccountsController): docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype] - tax_withholding_net_total = frappe.db.get_value( - doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"] + return frappe._dict( + frappe.db.get_all( + doctype, + filters={"name": ["in", docnames]}, + fields=["name", "base_tax_withholding_net_total"], + as_list=True, + ) ) - return tax_withholding_net_total - def apply_taxes(self): self.initialize_taxes() self.determine_exclusive_rate() diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 420fd3bee9d..de29ec0f004 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -139,6 +139,7 @@ class PricingRule(Document): self.validate_price_list_with_currency() self.validate_dates() self.validate_condition() + self.validate_mixed_with_recursion() if not self.margin_type: self.margin_rate_or_amount = 0.0 @@ -308,6 +309,10 @@ class PricingRule(Document): ): frappe.throw(_("Invalid condition expression")) + def validate_mixed_with_recursion(self): + if self.mixed_conditions and self.is_recursive: + frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system")) + # -------------------------------------------------------------------------------- diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 72961a6b6ec..87f6fa76bc8 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -1299,6 +1299,18 @@ class TestPricingRule(unittest.TestCase): item_group_rule.delete() item_code_rule.delete() + def test_validation_on_mixed_condition_with_recursion(self): + pricing_rule = make_pricing_rule( + discount_percentage=10, + selling=1, + priority=2, + min_qty=4, + title="_Test Pricing Rule with Min Qty - 2", + ) + pricing_rule.mixed_conditions = True + pricing_rule.is_recursive = True + self.assertRaises(frappe.ValidationError, pricing_rule.save) + test_dependencies = ["Campaign"] diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index ed4e3d2a8f3..71fe156dd7a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -146,6 +146,7 @@ class PromotionalScheme(Document): self.validate_applicable_for() self.validate_pricing_rules() + self.validate_mixed_with_recursion() def validate_applicable_for(self): if self.applicable_for: @@ -163,7 +164,7 @@ class PromotionalScheme(Document): docnames = [] # If user has changed applicable for - if self._doc_before_save.applicable_for == self.applicable_for: + if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for: return docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) @@ -177,6 +178,7 @@ class PromotionalScheme(Document): frappe.delete_doc("Pricing Rule", docname.name) def on_update(self): + self.validate() pricing_rules = ( frappe.get_all( "Pricing Rule", @@ -188,6 +190,15 @@ class PromotionalScheme(Document): ) self.update_pricing_rules(pricing_rules) + def validate_mixed_with_recursion(self): + if self.mixed_conditions: + if self.product_discount_slabs: + for slab in self.product_discount_slabs: + if slab.is_recursive: + frappe.throw( + _("Recursive Discounts with Mixed condition is not supported by the system") + ) + def update_pricing_rules(self, pricing_rules): rules = {} count = 0 diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 9b40c98829c..74ba6cf923c 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -129,6 +129,25 @@ class TestPromotionalScheme(unittest.TestCase): [pr.min_qty, pr.free_item, pr.free_qty, pr.recurse_for], [12, "_Test Item 2", 1, 12] ) + def test_validation_on_recurse_with_mixed_condition(self): + ps = make_promotional_scheme() + ps.set("price_discount_slabs", []) + ps.set( + "product_discount_slabs", + [ + { + "rule_description": "12+1", + "min_qty": 12, + "free_item": "_Test Item 2", + "free_qty": 1, + "is_recursive": 1, + "recurse_for": 12, + } + ], + ) + ps.mixed_conditions = True + self.assertRaises(frappe.ValidationError, ps.save) + def make_promotional_scheme(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 2106451bd1a..aa78f4b6204 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -109,7 +109,7 @@ def get_provisional_profit_loss( ): provisional_profit_loss = {} total_row = {} - if asset and (liability or equity): + if asset: total = total_row_total = 0 currency = currency or frappe.get_cached_value("Company", company, "default_currency") total_row = { @@ -122,14 +122,20 @@ def get_provisional_profit_loss( for period in period_list: key = period if consolidated else period.key - effective_liability = 0.0 - if liability: - effective_liability += flt(liability[-2].get(key)) - if equity: - effective_liability += flt(equity[-2].get(key)) + total_assets = flt(asset[0].get(key)) - provisional_profit_loss[key] = flt(asset[-2].get(key)) - effective_liability - total_row[key] = effective_liability + provisional_profit_loss[key] + if liability or equity: + effective_liability = 0.0 + if liability: + effective_liability += flt(liability[0].get(key)) + if equity: + effective_liability += flt(equity[0].get(key)) + + provisional_profit_loss[key] = total_assets - effective_liability + else: + provisional_profit_loss[key] = total_assets + + total_row[key] = provisional_profit_loss[key] if provisional_profit_loss[key]: has_value = True diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index e884deb2a17..4b20746286a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -2,10 +2,12 @@ # License: GNU General Public License v3. See license.txt +import copy from collections import OrderedDict import frappe from frappe import _, _dict +from frappe.query_builder import Criterion from frappe.utils import cstr, getdate from erpnext import get_company_currency, get_default_company @@ -17,9 +19,6 @@ from erpnext.accounts.report.financial_statements import get_cost_centers_with_c from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency from erpnext.accounts.utils import get_account_currency -# to cache translations -TRANSLATIONS = frappe._dict() - def execute(filters=None): if not filters: @@ -44,19 +43,11 @@ def execute(filters=None): columns = get_columns(filters) - update_translations() - res = get_result(filters, account_details) return columns, res -def update_translations(): - TRANSLATIONS.update( - dict(OPENING=_("Opening"), TOTAL=_("Total"), CLOSING_TOTAL=_("Closing (Opening + Total)")) - ) - - def validate_filters(filters, account_details): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) @@ -319,26 +310,31 @@ def get_accounts_with_children(accounts): if not isinstance(accounts, list): accounts = [d.strip() for d in accounts.strip().split(",") if d] - all_accounts = [] - for d in accounts: - account = frappe.get_cached_doc("Account", d) - if account: - children = frappe.get_all( - "Account", filters={"lft": [">=", account.lft], "rgt": ["<=", account.rgt]} - ) - all_accounts += [c.name for c in children] - else: - frappe.throw(_("Account: {0} does not exist").format(d)) + if not accounts: + return - return list(set(all_accounts)) if all_accounts else None + doctype = frappe.qb.DocType("Account") + accounts_data = ( + frappe.qb.from_(doctype) + .select(doctype.lft, doctype.rgt) + .where(doctype.name.isin(accounts)) + .run(as_dict=True) + ) + + conditions = [] + for account in accounts_data: + conditions.append((doctype.lft >= account.lft) & (doctype.rgt <= account.rgt)) + + return frappe.qb.from_(doctype).select(doctype.name).where(Criterion.any(conditions)).run(pluck=True) def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] + totals_dict = get_totals_dict() - gle_map = initialize_gle_map(gl_entries, filters) + gle_map = initialize_gle_map(gl_entries, filters, totals_dict) - totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map) + totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict) # Opening for filtered account data.append(totals.opening) @@ -387,9 +383,9 @@ def get_totals_dict(): ) return _dict( - opening=_get_debit_credit_dict(TRANSLATIONS.OPENING), - total=_get_debit_credit_dict(TRANSLATIONS.TOTAL), - closing=_get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL), + opening=_get_debit_credit_dict(_("Opening")), + total=_get_debit_credit_dict(_("Total")), + closing=_get_debit_credit_dict(_("Closing (Opening + Total)")), ) @@ -402,17 +398,16 @@ def group_by_field(group_by): return "voucher_no" -def initialize_gle_map(gl_entries, filters): +def initialize_gle_map(gl_entries, filters, totals_dict): gle_map = OrderedDict() group_by = group_by_field(filters.get("group_by")) for gle in gl_entries: - gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) + gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[])) return gle_map -def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): - totals = get_totals_dict() +def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals): entries = [] consolidated_gle = OrderedDict() group_by = group_by_field(filters.get("group_by")) 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 3a3cedc0bae..276e95bd5d1 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -12,7 +12,7 @@ def execute(filters=None): else: party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name") - filters.update({"naming_series": party_naming_by}) + filters["naming_series"] = party_naming_by validate_filters(filters) ( @@ -63,21 +63,23 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ tax_withholding_category = tds_accounts.get(entry.account) # or else the consolidated value from the voucher document if not tax_withholding_category: - tax_withholding_category = tax_category_map.get(name) + tax_withholding_category = tax_category_map.get((voucher_type, name)) # or else from the party default if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") rate = tax_rate_map.get(tax_withholding_category) - if net_total_map.get(name): + if net_total_map.get((voucher_type, name)): if voucher_type == "Journal Entry": # back calcalute total amount from rate and tax_amount if rate: total_amount = grand_total = base_total = tax_amount / (rate / 100) elif voucher_type == "Purchase Invoice": - total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name) + total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get( + (voucher_type, name) + ) else: - total_amount, grand_total, base_total = net_total_map.get(name) + total_amount, grand_total, base_total = net_total_map.get((voucher_type, name)) else: total_amount += entry.credit @@ -97,7 +99,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ } if filters.naming_series == "Naming Series": - row.update({"party_name": party_map.get(party, {}).get(party_name)}) + row["party_name"] = party_map.get(party, {}).get(party_name) row.update( { @@ -279,7 +281,6 @@ def get_tds_docs(filters): journal_entries = [] tax_category_map = frappe._dict() net_total_map = frappe._dict() - frappe._dict() journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") @@ -412,7 +413,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): ) for entry in entries: - tax_category_map.update({entry.name: entry.tax_withholding_category}) + tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category if doctype == "Purchase Invoice": value = [ entry.base_tax_withholding_net_total, @@ -427,7 +428,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount] else: value = [entry.total_amount] * 3 - net_total_map.update({entry.name: value}) + + net_total_map[(doctype, entry.name)] = value def get_tax_rate_map(filters): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 489fbaca6b2..9284c86bd92 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -20,6 +20,15 @@ frappe.ui.form.on("Asset Repair", { }; }; + frm.set_query("asset", function () { + return { + filters: { + company: frm.doc.company, + docstatus: 1, + }, + }; + }); + frm.set_query("warehouse", "stock_items", function () { return { filters: { diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index b23c3f50b9a..cf5e0ca3f8f 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -39,6 +39,7 @@ def validate_filters(filters): def get_data(filters): po = frappe.qb.DocType("Purchase Order") po_item = frappe.qb.DocType("Purchase Order Item") + pi = frappe.qb.DocType("Purchase Invoice") pi_item = frappe.qb.DocType("Purchase Invoice Item") query = ( @@ -46,6 +47,8 @@ def get_data(filters): .from_(po_item) .left_join(pi_item) .on(pi_item.po_detail == po_item.name) + .left_join(pi) + .on(pi.name == pi_item.parent) .select( po.transaction_date.as_("date"), po_item.schedule_date.as_("required_date"), @@ -69,6 +72,7 @@ def get_data(filters): po_item.name, ) .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1)) + .where(pi.docstatus == 1) .groupby(po_item.name) .orderby(po.transaction_date) ) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e9896fa0fd6..71e40d509b2 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -28,7 +28,7 @@ class SellingController(StockController): def validate(self): super().validate() self.validate_items() - if not self.get("is_debit_note"): + if not (self.get("is_debit_note") or self.get("is_return")): self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 492000d71d4..e8aa6254d34 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -94,7 +94,10 @@ status_map = { ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], ["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], - ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], + [ + "Completed", + "eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)", + ], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 212c9adf4af..1620fa3bfff 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -218,7 +218,7 @@ class StockController(AccountsController): "do_not_submit": True if not via_landed_cost_voucher else False, } - if row.get("qty") or row.get("consumed_qty"): + if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"): self.update_bundle_details(bundle_details, table_name, row) self.create_serial_batch_bundle(bundle_details, row) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index e236e7a6345..71b23ba3202 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -156,12 +156,12 @@ class BOMCreator(Document): amount = self.get_raw_material_cost() self.raw_material_cost = amount - def get_raw_material_cost(self, fg_reference_id=None, amount=0): - if not fg_reference_id: - fg_reference_id = self.name + def get_raw_material_cost(self, fg_item=None, amount=0): + if not fg_item: + fg_item = self.item_code for row in self.items: - if row.fg_reference_id != fg_reference_id: + if row.fg_item != fg_item: continue if not row.is_expandable: @@ -183,7 +183,7 @@ class BOMCreator(Document): else: row.amount = 0.0 - row.amount = self.get_raw_material_cost(row.name, row.amount) + row.amount = self.get_raw_material_cost(row.item_code, row.amount) row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor)) amount += flt(row.amount) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 66e98dde5b4..b661e6f99a4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -369,3 +369,4 @@ erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_de erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry erpnext.patches.v15_0.update_total_number_of_booked_depreciations +erpnext.patches.v15_0.do_not_use_batchwise_valuation diff --git a/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py b/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py new file mode 100644 index 00000000000..9e95a34acc7 --- /dev/null +++ b/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") + if valuation_method in ["FIFO", "LIFO"]: + return + + if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1): + return + + if frappe.get_all("Item", filters={"has_batch_no": 1, "valuation_method": "FIFO"}, limit=1): + return + + frappe.db.set_single_value("Stock Settings", "do_not_use_batchwise_valuation", 1) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 06eced2036f..b09735e5644 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -262,8 +262,7 @@ class Project(Document): if self.status == "Cancelled": return - if self.percent_complete == 100: - self.status = "Completed" + self.status = "Completed" if self.percent_complete == 100 else "Open" def update_costing(self): from frappe.query_builder.functions import Max, Min, Sum diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 950914953aa..dfb0ded8139 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1750,12 +1750,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_product_discount(args) { const items = this.frm.doc.items.filter(d => (d.is_free_item)) || []; - const exist_items = items.map(row => (row.item_code, row.pricing_rules)); + const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};}); args.free_item_data.forEach(pr_row => { let row_to_modify = {}; - if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) { + // If there are no free items, or if the current free item doesn't exist in the table, add it + if (!items || !exist_items.filter(e_row => { + return e_row.item_code == pr_row.item_code && e_row.pricing_rules == pr_row.pricing_rules; + }).length) { row_to_modify = frappe.model.add_child(this.frm.doc, this.frm.doc.doctype + ' Item', 'items'); diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 0b482c3292f..91a85aa02a5 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -220,6 +220,28 @@ erpnext.sales_common = { if (doc.docstatus === 0 && doc.is_return && !doc.return_against) { frappe.model.set_value(cdt, cdn, "incoming_rate", 0.0); } + + this.set_actual_qty(doc, cdt, cdn); + } + + set_actual_qty(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + let sales_doctypes = ["Sales Invoice", "Delivery Note", "Sales Order"]; + + if (row.item_code && row.warehouse && sales_doctypes.includes(doc.doctype)) { + frappe.call({ + method: "erpnext.stock.get_item_details.get_bin_details", + args: { + item_code: row.item_code, + warehouse: row.warehouse, + }, + callback(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "actual_qty", r.message.actual_qty); + } + }, + }); + } } toggle_editable_price_list_rate() { diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js index 7aa8012f0b6..80dd246eb8a 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js @@ -36,7 +36,6 @@ frappe.ui.form.on("Import Supplier Invoice", { toggle_read_only_fields: function (frm) { if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) { cur_frm.set_read_only(); - cur_frm.refresh_fields(); frm.set_df_property("import_invoices", "hidden", 1); } else { frm.set_df_property("import_invoices", "hidden", 0); diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 6932f7be825..0216f75a628 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -43,6 +43,7 @@ class HolidayList(Document): self.validate_days() self.total_holidays = len(self.holidays) self.validate_duplicate_date() + self.sort_holidays() @frappe.whitelist() def get_weekly_off_dates(self): @@ -57,8 +58,6 @@ class HolidayList(Document): self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1}) - self.sort_holidays() - @frappe.whitelist() def get_supported_countries(self): from holidays.utils import list_supported_countries @@ -100,8 +99,6 @@ class HolidayList(Document): "holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0} ) - self.sort_holidays() - def sort_holidays(self): self.holidays.sort(key=lambda x: getdate(x.holiday_date)) for i in range(len(self.holidays)): diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index e490badfc40..c539c315747 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -158,6 +158,10 @@ class Batch(Document): def set_batchwise_valuation(self): if self.is_new(): + if frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"): + self.use_batchwise_valuation = 0 + return + self.use_batchwise_valuation = 1 def before_save(self): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 3352b53343a..1f6816e3fed 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -223,7 +223,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( ); } - if (doc.docstatus == 1 && frappe.model.can_create("Delivery Trip")) { + if (doc.docstatus == 1 && doc.status != "Completed" && frappe.model.can_create("Delivery Trip")) { this.frm.add_custom_button( __("Delivery Trip"), function () { diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f24c912e362..5f44548a873 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1654,7 +1654,7 @@ class TestDeliveryNote(FrappeTestCase): { "is_stock_item": 1, "has_batch_no": 1, - "batch_no_series": "BATCH-TESTSERIAL-.#####", + "batch_number_series": "BATCH-TESTSERIAL-.#####", "create_new_batch": 1, }, ) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 4f8649c0bfa..2445d1f66ea 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -58,10 +58,12 @@ frappe.ui.form.on("Delivery Trip", { date_field: "posting_date", setters: { company: frm.doc.company, + customer: null, }, get_query_filters: { docstatus: 1, company: frm.doc.company, + status: ["Not In", ["Completed", "Cancelled"]], }, }); }, diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 0e4055251f0..9638c4e6289 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -189,7 +189,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-05 12:52:18.705431", + "modified": "2024-07-08 08:58:50.228211", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", @@ -221,19 +221,11 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "write": 1 + "role": "Stock User" } ], "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 103d8a1ae57..3bafa12983f 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -33,7 +33,7 @@ class InventoryDimension(Document): dimension_name: DF.Data disabled: DF.Check document_type: DF.Link | None - fetch_from_parent: DF.Literal + fetch_from_parent: DF.Literal[None] istable: DF.Check mandatory_depends_on: DF.SmallText | None reference_document: DF.Link diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 6dd53f290c9..066dba95b1e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -6,7 +6,7 @@ from collections import OrderedDict, defaultdict from itertools import groupby import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case @@ -73,6 +73,9 @@ class PickList(Document): def validate(self): self.validate_for_qty() + if self.pick_manually and self.get("locations"): + self.validate_stock_qty() + self.check_serial_no_status() def before_save(self): self.update_status() @@ -82,6 +85,60 @@ class PickList(Document): if self.get("locations"): self.validate_sales_order_percentage() + def validate_stock_qty(self): + from erpnext.stock.doctype.batch.batch import get_batch_qty + + for row in self.get("locations"): + if row.batch_no and not row.qty: + batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code) + + if row.qty > batch_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}." + ).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + continue + + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": row.item_code, "warehouse": row.warehouse}, + "actual_qty", + ) + + if row.qty > bin_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." + ).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + def check_serial_no_status(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in self.get("locations"): + if not row.serial_no: + continue + + picked_serial_nos = get_serial_nos(row.serial_no) + validated_serial_nos = frappe.get_all( + "Serial No", + filters={"name": ("in", picked_serial_nos), "warehouse": row.warehouse}, + pluck="name", + ) + + incorrect_serial_nos = set(picked_serial_nos) - set(validated_serial_nos) + if incorrect_serial_nos: + frappe.throw( + _("The Serial No at Row #{0}: {1} is not available in warehouse {2}.").format( + row.idx, ", ".join(incorrect_serial_nos), row.warehouse + ), + title=_("Incorrect Warehouse"), + ) + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 116a0bd833d..b1c03bf8453 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -648,7 +648,7 @@ class TestPickList(FrappeTestCase): def test_picklist_for_batch_item(self): warehouse = "_Test Warehouse - _TC" item = make_item( - properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"} + properties={"is_stock_item": 1, "has_batch_no": 1, "batch_number_series": "PICKLT-.######"} ).name # create batch @@ -1132,3 +1132,45 @@ class TestPickList(FrappeTestCase): pl.save() self.assertEqual(pl.locations[0].qty, 80.0) + + def test_validate_picked_qty_with_manual_option(self): + warehouse = "_Test Warehouse - _TC" + non_serialized_item = make_item( + "Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1} + ).name + + serialized_item = make_item( + "Test Serialized Pick List Item For Manual Option", + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"}, + ).name + + batched_item = make_item( + "Test Batched Pick List Item For Manual Option", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "SN-HBNMSPLI-.####", + "create_new_batch": 1, + }, + ).name + + make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100) + + so = make_sales_order( + item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse + ) + so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.set_missing_values() + so.save() + so.submit() + + pl = create_pick_list(so.name) + pl.pick_manually = 1 + + for row in pl.locations: + row.qty = row.qty + 10 + + self.assertRaises(frappe.ValidationError, pl.save) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index ee8df084f08..61a180caba4 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1111,6 +1111,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "per_returned", "fieldtype": "Percent", + "in_list_view": 1, "label": "% Returned", "no_copy": 1, "print_hide": 1, @@ -1272,7 +1273,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-04-08 20:23:03.699201", + "modified": "2024-07-04 14:50:10.538472", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a8cb7fd83b4..3c97d13b904 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -785,7 +785,6 @@ class PurchaseReceipt(BuyingController): def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) - is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): @@ -810,26 +809,10 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked - stock_rbnb = ( - self.get("asset_received_but_not_billed") - if is_asset_pr - else self.get_company_default("stock_received_but_not_billed") - ) i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): - if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item(): - account = tax.account_head - else: - negative_expense_booked_in_pi = frappe.db.sql( - """select name from `tabPurchase Invoice Item` pi - where docstatus = 1 and purchase_receipt=%s - and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", - (self.name, tax.account_head), - ) - account = stock_rbnb if negative_expense_booked_in_pi else tax.account_head - + account = tax.account_head if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 968cb68dac0..869d3ed3cce 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -8,6 +8,7 @@ from pypika import functions as fn import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.item.test_item import create_item, make_item @@ -3170,6 +3171,185 @@ class TestPurchaseReceipt(FrappeTestCase): lcv.save().submit() return lcv + def test_tax_account_heads_on_item_repost_without_lcv(self): + """ + PO -> PR -> PI + Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb if Purchase Receipt was created first + This scenario is without LCV + """ + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_purchase_order, + make_pr_against_po, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice + + stock_rbnb = "Stock Received But Not Billed - _TC" + stock_in_hand = "Stock In Hand - _TC" + test_cc = "_Test Cost Center - _TC" + test_company = "_Test Company" + creditors = "Creditors - _TC" + + company_doc = frappe.get_doc("Company", test_company) + company_doc.enable_perpetual_inventory = True + company_doc.stock_received_but_not_billed = stock_rbnb + company_doc.default_inventory_account = stock_in_hand + company_doc.save() + + packaging_charges_account = create_account( + account_name="Packaging Charges", + parent_account="Indirect Expenses - _TC", + company=test_company, + account_type="Tax", + ) + + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + po.taxes = [] + po.append( + "taxes", + { + "category": "Valuation and Total", + "account_head": packaging_charges_account, + "cost_center": test_cc, + "description": "Test", + "add_deduct_tax": "Add", + "charge_type": "Actual", + "tax_amount": 250, + }, + ) + po.save().submit() + + pr = make_pr_against_po(po.name, received_qty=10) + pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + ] + self.assertEqual(expected_pr_gles, pr_gl_entries) + + # Make PI against Purchase Receipt + pi = make_purchase_invoice(pr.name).save().submit() + pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True) + expected_pi_gles = [ + {"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None}, + ] + self.assertEqual(expected_pi_gles, pi_gl_entries) + + # Trigger Repost Item Valudation on a older date + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Item and Warehouse", + "item_code": pr.items[0].item_code, + "warehouse": pr.items[0].warehouse, + "posting_date": add_days(pr.posting_date, -1), + "posting_time": "00:00:00", + "company": pr.company, + "allow_negative_stock": 1, + "via_landed_cost_voucher": 0, + "allow_zero_rate": 0, + } + ) + repost_doc.save().submit() + + pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles_after_repost = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + ] + self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost)) + self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost) + + # teardown + pi.reload() + pi.cancel() + pr.reload() + pr.cancel() + + company_doc.enable_perpetual_inventory = False + company_doc.stock_received_but_not_billed = None + company_doc.default_inventory_account = None + company_doc.save() + + def test_do_not_use_batchwise_valuation_rate(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Test Item for Do Not Use Batchwise Valuation" + make_item( + item_code, + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TIDNBV-.#####", + "valuation_method": "Moving Average", + }, + ) + + # 1st pr for 100 rate + pr = make_purchase_receipt( + item_code=item_code, + qty=1, + rate=100, + posting_date=add_days(today(), -2), + ) + + make_purchase_receipt( + item_code=item_code, + qty=1, + rate=200, + posting_date=add_days(today(), -1), + ) + + dn = create_delivery_note( + item_code=item_code, + qty=1, + rate=300, + posting_date=today(), + use_serial_batch_fields=1, + batch_no=get_batch_from_bundle(pr.items[0].serial_and_batch_bundle), + ) + dn.reload() + bundle = dn.items[0].serial_and_batch_bundle + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 100) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 1 + doc.flags.ignore_validate = True + doc.save() + + pr.repost_future_sle_and_gle(force=True) + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 150) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 0 + doc.flags.ignore_validate = True + doc.save() + + def test_status_mapping(self): + item_code = "item_for_status" + create_item(item_code) + create_item("item_for_status") + warehouse = create_warehouse("Stores") + supplier = "Test Supplier" + create_supplier(supplier_name=supplier) + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=1, + rate=0, + ) + self.assertEqual(pr.grand_total, 0.0) + self.assertEqual(pr.status, "Completed") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -3412,7 +3592,6 @@ def make_purchase_receipt(**args): pr.insert() if not args.do_not_submit: pr.submit() - pr.load_from_db() return pr diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index a003f4986f9..f251fea16ee 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -111,10 +111,11 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-25 20:39:19.973437", + "modified": "2024-07-08 09:19:26.711470", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -130,16 +131,10 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, "print": 1, "read": 1, "report": 1, - "role": "Stock User", - "share": 1, - "write": 1 + "role": "Stock User" }, { "email": 1, @@ -157,4 +152,4 @@ "sort_order": "DESC", "title_field": "item_code", "track_changes": 1 -} \ No newline at end of file +} 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 63e1e5084d6..b8b17cb4c81 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 @@ -228,6 +228,16 @@ class SerialandBatchBundle(Document): def get_serial_nos(self): return [d.serial_no for d in self.entries if d.serial_no] + def update_valuation_rate(self, valuation_rate=None, save=False): + for row in self.entries: + row.incoming_rate = valuation_rate + row.stock_value_difference = flt(row.qty) * flt(valuation_rate) + + if save: + row.db_set( + {"incoming_rate": row.incoming_rate, "stock_value_difference": row.stock_value_difference} + ) + def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False): sle = self.get_sle_for_outward_transaction() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 2913af4a724..32f3b4fb85b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -653,7 +653,7 @@ class TestSerialandBatchBundle(FrappeTestCase): "is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, - "batch_no_series": "PSNBI-TSNVL-.#####", + "batch_number_series": "PSNBI-TSNVL-.#####", "has_serial_no": 1, "serial_no_series": "SN-PSNBI-TSNVL-.#####", }, diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index eee38be0278..db8e1fef69b 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -29,7 +29,7 @@ } ], "links": [], - "modified": "2021-05-21 11:27:01.144110", + "modified": "2024-07-08 08:41:19.385020", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", @@ -72,20 +72,12 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "write": 1 + "role": "Stock User" } ], "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index f03cc49ef73..5d9ddaccedf 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -44,6 +44,7 @@ "auto_reserve_serial_and_batch", "serial_and_batch_item_settings_tab", "section_break_7", + "do_not_use_batchwise_valuation", "auto_create_serial_and_batch_bundle_for_outward", "pick_serial_and_batch_based_on", "column_break_mhzc", @@ -437,6 +438,14 @@ "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle", "fieldtype": "Check", "label": "Do Not Update Serial / Batch on Creation of Auto Bundle" + }, + { + "default": "0", + "depends_on": "eval:doc.valuation_method === \"Moving Average\"", + "description": "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.", + "fieldname": "do_not_use_batchwise_valuation", + "fieldtype": "Check", + "label": "Do Not Use Batch-wise Valuation" } ], "icon": "icon-cog", @@ -444,7 +453,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-02-25 16:32:01.084453", + "modified": "2024-07-04 12:45:09.811280", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 24e8dccd17c..e786b1f67c6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -40,6 +40,7 @@ class StockSettings(Document): default_warehouse: DF.Link | None disable_serial_no_and_batch_selector: DF.Check do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check + do_not_use_batchwise_valuation: DF.Check enable_stock_reservation: DF.Check item_group: DF.Link | None item_naming_by: DF.Literal["Item Code", "Naming Series"] @@ -98,6 +99,22 @@ class StockSettings(Document): self.validate_stock_reservation() self.change_precision_for_for_sales() self.change_precision_for_purchase() + self.validate_use_batch_wise_valuation() + + def validate_use_batch_wise_valuation(self): + if not self.do_not_use_batchwise_valuation: + return + + if self.valuation_method == "FIFO": + frappe.throw(_("Cannot disable batch wise valuation for FIFO valuation method.")) + + if frappe.get_all( + "Item", filters={"valuation_method": "FIFO", "is_stock_item": 1, "has_batch_no": 1}, limit=1 + ): + frappe.throw(_("Can't disable batch wise valuation for items with FIFO valuation method.")) + + if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1): + frappe.throw(_("Can't disable batch wise valuation for active batches.")) def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d0195843c5f..ff4798515c3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -530,6 +530,10 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") + self.use_moving_avg_for_batch = frappe.db.get_single_value( + "Stock Settings", "do_not_use_batchwise_valuation" + ) + self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( item_code=self.item_code ) @@ -745,7 +749,7 @@ class update_entries_after: if sle.get(dimension.get("fieldname")): has_dimensions = True - if sle.serial_and_batch_bundle: + if sle.serial_and_batch_bundle and (not self.use_moving_avg_for_batch or sle.has_serial_no): self.calculate_valuation_for_serial_batch_bundle(sle) elif sle.serial_no and not self.args.get("sle_id"): # Only run in reposting @@ -765,7 +769,12 @@ class update_entries_after: # Only run in reposting self.update_batched_values(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: + if ( + sle.voucher_type == "Stock Reconciliation" + and not sle.batch_no + and not sle.has_batch_no + and not has_dimensions + ): # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -806,6 +815,15 @@ class update_entries_after: sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() + if ( + sle.serial_and_batch_bundle + and self.valuation_method == "Moving Average" + and self.use_moving_avg_for_batch + and (sle.batch_no or sle.has_batch_no) + ): + valuation_rate = flt(stock_value_difference) / flt(sle.actual_qty) + self.update_valuation_rate_in_serial_and_batch_bundle(sle, valuation_rate) + if not self.args.get("sle_id") or ( sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle ): @@ -916,6 +934,21 @@ class update_entries_after: self.wh_data.qty_after_transaction, precision ) + def update_valuation_rate_in_serial_and_batch_bundle(self, sle, valuation_rate): + # Only execute if the item has batch_no and the valuation method is moving average + if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): + return + + doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) + doc.update_valuation_rate(valuation_rate, save=True) + doc.calculate_qty_and_amount(save=True) + + def get_outgoing_rate_for_batched_item(self, sle): + if self.wh_data.qty_after_transaction == 0: + return 0 + + return flt(self.wh_data.stock_value) / flt(self.wh_data.qty_after_transaction) + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 4c502e106ec..51d2708be24 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -244,6 +244,8 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): "Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1 ) + use_moving_avg_for_batch = frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation") + if isinstance(args, dict): args = frappe._dict(args) @@ -257,7 +259,12 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): return sn_obj.get_incoming_rate() - elif item_details and item_details.has_batch_no and args.get("serial_and_batch_bundle"): + elif ( + item_details + and item_details.has_batch_no + and args.get("serial_and_batch_bundle") + and not use_moving_avg_for_batch + ): args.actual_qty = args.qty batch_obj = BatchNoValuation( sle=args, @@ -274,7 +281,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): sn_obj = SerialNoValuation(sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code")) return sn_obj.get_incoming_rate() - elif args.get("batch_no") and not args.get("serial_and_batch_bundle"): + elif args.get("batch_no") and not args.get("serial_and_batch_bundle") and not use_moving_avg_for_batch: args.actual_qty = args.qty args.batch_nos = frappe._dict({args.batch_no: args}) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5a53a5ff2bd..3a7911ae49a 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1231,6 +1231,8 @@ Is Default,Ist Standard, Is Existing Asset,Vermögenswert existiert bereits., Is Frozen,Ist gesperrt, Is Group,Ist Gruppe, +Is Group Warehouse,Ist Lagergruppe, +Is Rejected Warehouse,Ist Lager für abgelehnte Ware, Issue,Anfrage, Issue Material,Material ausgeben, Issued,Ausgestellt, @@ -1294,6 +1296,7 @@ Items for Raw Material Request,Artikel für Rohstoffanforderung, Job Card,Jobkarte, Job card {0} created,Jobkarte {0} erstellt, Join,Beitreten, +Joining,Beitritt, Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft, Journal Entry,Buchungssatz, Journal Entry {0} does not have account {1} or already matched against other voucher,Buchungssatz {0} gehört nicht zu Konto {1} oder ist bereits mit einem anderen Beleg abgeglichen, @@ -1993,6 +1996,7 @@ Product Search,Produkt Suche, Production,Produktion, Production Item,Produktions-Artikel, Products,Produkte, +Profile,Profil, Profit and Loss,Gewinn und Verlust, Profit for the year,Jahresüberschuss, Program,Programm, @@ -2054,6 +2058,9 @@ Qty To Manufacture,Herzustellende Menge, Qty Total,Gesamtmenge, Qty for {0},Menge für {0}, Qualification,Qualifikation, +Qualification Status,Qualifikationsstatus, +Qualified By,Qualifiziert von, +Qualified on,Qualifiziert am, Quality,Qualität, Quality Action,Qualitätsmaßnahme, Quality Goal.,Qualitätsziel., @@ -3227,6 +3234,7 @@ Warehouse Type,Lagertyp, 'Date' is required,'Datum' ist erforderlich, Budgets,Budgets, Bundle Qty,Bundle Menge, +Company Details,Unternehmensdetails, Company GSTIN,Unternehmen GSTIN, Company field is required,Firmenfeld ist erforderlich, Creating Dimensions...,Dimensionen erstellen ..., @@ -3562,6 +3570,7 @@ Performance,Performance, Period based On,Zeitraum basierend auf, Perpetual inventory required for the company {0} to view this report.,"Permanente Bestandsaufnahme erforderlich, damit das Unternehmen {0} diesen Bericht anzeigen kann.", Phone,Telefon, +Phone Ext.,Telefon Ext., Pick List,Auswahlliste, Plaid authentication error,Plaid-Authentifizierungsfehler, Plaid public token error,Plaid public token error, @@ -4970,7 +4979,7 @@ Number of Depreciations Booked,Anzahl der gebuchten Abschreibungen, Finance Books,Finanzbücher, Straight Line,Gerade Linie, Double Declining Balance,Doppelte degressive, -Manual,Handbuch, +Manual,Manuell, Value After Depreciation,Wert nach Abschreibung, Total Number of Depreciations,Gesamtzahl der Abschreibungen, Frequency of Depreciation (Months),Die Häufigkeit der Abschreibungen (Monate), @@ -5030,6 +5039,7 @@ Maintenance Team Name,Name des Wartungsteams, Maintenance Team Members,Mitglieder des Wartungsteams, Purpose,Zweck, Stock Manager,Lagerleiter, +Stock Movement,Lagerbewegung, Asset Movement Item,Vermögensbewegungsgegenstand, Source Location,Quellspeicherort, From Employee,Von Mitarbeiter, @@ -5126,6 +5136,7 @@ Default Bank Account,Standardbankkonto, Is Transporter,Ist Transporter, Represents Company,Repräsentiert das Unternehmen, Supplier Type,Lieferantentyp, +Allow Purchase,Einkauf zulassen, Allow Purchase Invoice Creation Without Purchase Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen, Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Eingangsbeleg zulassen, Warn RFQs,Warnung Ausschreibungen, @@ -5899,6 +5910,7 @@ Occupational Hazards and Environmental Factors,Berufsrisiken und Umweltfaktoren, Other Risk Factors,Andere Risikofaktoren, Patient Details,Patientendetails, Additional information regarding the patient,Zusätzliche Informationen zum Patienten, +Additional Info,Zusätzliche Informationen, HLC-APP-.YYYY.-,HLC-APP-.YYYY.-, Patient Age,Patient Alter, Get Prescribed Clinical Procedures,Holen Sie sich vorgeschriebene klinische Verfahren, @@ -6059,6 +6071,7 @@ Date Of Retirement,Zeitpunkt der Pensionierung, Department and Grade,Abteilung und Klasse, Reports to,Vorgesetzter, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, +Attendance & Leaves,Anwesenheit & Urlaub, Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), Applicable Holiday List,Geltende Feiertagsliste, Default Shift,Standardverschiebung, @@ -6519,7 +6532,7 @@ Default Costing Rate,Standardkosten, Default Billing Rate,Standard-Rechnungspreis, Dependent Task,Abhängiger Vorgang, Project Type,Projekttyp, -% Complete Method,% abgeschlossene Methode, +% Complete Method,Fertigstellung bemessen nach, Task Completion,Aufgabenerledigung, Task Progress,Vorgangsentwicklung, % Completed,% abgeschlossen, @@ -6675,6 +6688,7 @@ Restaurant Reservation,Restaurant Reservierung, Waitlisted,Auf der Warteliste, No Show,Nicht angetreten, No of People,Anzahl von Personen, +No of Employees,Anzahl der Mitarbeiter, Reservation Time,Reservierungszeit, Reservation End Time,Reservierungsendzeit, No of Seats,Anzahl der Sitze, @@ -6686,6 +6700,7 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens, From Lead,Aus Lead, Account Manager,Kundenberater, Accounts Manager,Buchhalter, +Allow Sales,Verkauf zulassen, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein, Default Price List,Standardpreisliste, @@ -6745,7 +6760,8 @@ Not Delivered,Nicht geliefert, Fully Delivered,Komplett geliefert, Partly Delivered,Teilweise geliefert, Not Applicable,Nicht andwendbar, -% Delivered,% geliefert, +% Delivered,% Geliefert, +% Picked,% Kommissioniert, % of materials delivered against this Sales Order,% der für diesen Auftrag gelieferten Materialien, % of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden, Not Billed,Nicht abgerechnet, @@ -6879,6 +6895,7 @@ New Income,Neuer Verdienst, New Expenses,Neue Ausgaben, Annual Income,Jährliches Einkommen, Annual Expenses,Jährliche Kosten, +Annual Revenue,Jährlicher Umsatz, Bank Balance,Kontostand, Bank Credit Balance,Bankguthaben, Receivables,Forderungen, @@ -7352,6 +7369,7 @@ Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen, Stock Value Difference,Lagerwert-Differenz, Stock Queue (FIFO),Lagerverfahren (FIFO), Is Cancelled,Ist storniert, +Is Cash or Non Trade Discount,Ist Bar- oder Nicht-Handelsrabatt, Stock Reconciliation,Bestandsabgleich, This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Dieses Werkzeug hilft Ihnen dabei, die Menge und die Bewertung von Bestand im System zu aktualisieren oder zu ändern. Es wird in der Regel verwendet, um die Systemwerte und den aktuellen Bestand Ihrer Lager zu synchronisieren.", Reconciliation JSON,Abgleich JSON (JavaScript Object Notation), @@ -7453,6 +7471,7 @@ Warranty / AMC Status,Status der Garantie / des jährlichen Wartungsvertrags, Resolved By,Entschieden von, Service Address,Serviceadresse, If different than customer address,Falls abweichend von Kundenadresse, +"If yes, then this warehouse will be used to store rejected materials","Falls aktiviert, wird dieses Lager verwendet, um abgelehnte Ware zu lagern", Raised By,Gemeldet durch, From Company,Von Unternehmen, Rename Tool,Werkzeug zum Umbenennen, @@ -8628,6 +8647,7 @@ Print Receipt,Druckeingang, Edit Receipt,Beleg bearbeiten, Focus on search input,Konzentrieren Sie sich auf die Sucheingabe, Focus on Item Group filter,Fokus auf Artikelgruppenfilter, +Footer will display correctly only in PDF,Die Fußzeile wird nur im PDF korrekt angezeigt, Checkout Order / Submit Order / New Order,Kaufabwicklung / Bestellung abschicken / Neue Bestellung, Add Order Discount,Bestellrabatt hinzufügen, Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar.,