Merge pull request #42253 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-07-10 16:12:52 +05:30
committed by GitHub
44 changed files with 822 additions and 248 deletions

View File

@@ -120,30 +120,41 @@ frappe.ui.form.on("Bank Statement Import", {
}, },
show_import_status(frm) { show_import_status(frm) {
let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); if (frm.doc.status == "Pending") return;
let successful_records = import_log.filter((log) => log.success);
let failed_records = import_log.filter((log) => !log.success); frappe.call({
if (successful_records.length === 0) return; 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; let message;
if (failed_records.length === 0) { if (failed_records === 0) {
let message_args = [successful_records.length]; let message_args = [successful_records];
if (frm.doc.import_type === "Insert New Records") { if (frm.doc.import_type === "Insert New Records") {
message = message =
successful_records.length > 1 successful_records > 1
? __("Successfully imported {0} records.", message_args) ? __("Successfully imported {0} records.", message_args)
: __("Successfully imported {0} record.", message_args); : __("Successfully imported {0} record.", message_args);
} else { } else {
message = message =
successful_records.length > 1 successful_records > 1
? __("Successfully updated {0} records.", message_args) ? __("Successfully updated {0} records.", message_args)
: __("Successfully updated {0} record.", message_args); : __("Successfully updated {0} record.", message_args);
} }
} else { } else {
let message_args = [successful_records.length, import_log.length]; let message_args = [successful_records, total_records];
if (frm.doc.import_type === "Insert New Records") { if (frm.doc.import_type === "Insert New Records") {
message = message =
successful_records.length > 1 successful_records > 1
? __( ? __(
"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args message_args
@@ -154,7 +165,7 @@ frappe.ui.form.on("Bank Statement Import", {
); );
} else { } else {
message = message =
successful_records.length > 1 successful_records > 1
? __( ? __(
"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args message_args
@@ -165,8 +176,11 @@ frappe.ui.form.on("Bank Statement Import", {
); );
} }
} }
frm.dashboard.set_headline(message); frm.dashboard.set_headline(message);
}, },
});
},
show_report_error_button(frm) { show_report_error_button(frm) {
if (frm.doc.status === "Error") { if (frm.doc.status === "Error") {
@@ -287,7 +301,7 @@ frappe.ui.form.on("Bank Statement Import", {
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
show_import_preview(frm, preview_data) { 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) { if (frm.import_preview && frm.import_preview.doctype === frm.doc.reference_doctype) {
frm.import_preview.preview_data = preview_data; 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) { show_import_warnings(frm, preview_data) {
let columns = preview_data.columns; let columns = preview_data.columns;
let warnings = JSON.parse(frm.doc.template_warnings || "[]"); let warnings = JSON.parse(frm.doc.template_warnings || "[]");
@@ -401,16 +424,18 @@ frappe.ui.form.on("Bank Statement Import", {
frm.trigger("show_import_log"); frm.trigger("show_import_log");
}, },
show_import_log(frm) { render_import_log(frm) {
let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); frappe.call({
let logs = import_log; method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.get_import_logs",
frm.toggle_display("import_log", false); args: {
frm.toggle_display("import_log_section", logs.length > 0); docname: frm.doc.name,
},
callback: function (r) {
let logs = r.message;
if (logs.length === 0) { if (logs.length === 0) return;
frm.get_field("import_log_preview").$wrapper.empty();
return; frm.toggle_display("import_log_section", true);
}
let rows = logs let rows = logs
.map((log) => { .map((log) => {
@@ -434,8 +459,7 @@ frappe.ui.form.on("Bank Statement Import", {
]); ]);
} }
} else { } else {
let messages = log.messages let messages = JSON.parse(log.messages || "[]")
.map(JSON.parse)
.map((m) => { .map((m) => {
let title = m.title ? `<strong>${m.title}</strong>` : ""; let title = m.title ? `<strong>${m.title}</strong>` : "";
let message = m.message ? `<div>${m.message}</div>` : ""; let message = m.message ? `<div>${m.message}</div>` : "";
@@ -461,7 +485,7 @@ frappe.ui.form.on("Bank Statement Import", {
} }
return `<tr> return `<tr>
<td>${log.row_indexes.join(", ")}</td> <td>${JSON.parse(log.row_indexes).join(", ")}</td>
<td> <td>
<div class="indicator ${indicator_color}">${title}</div> <div class="indicator ${indicator_color}">${title}</div>
</td> </td>
@@ -489,4 +513,33 @@ frappe.ui.form.on("Bank Statement Import", {
</table> </table>
`); `);
}, },
});
},
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"));
}
},
});
},
}); });

View File

@@ -11,6 +11,8 @@
"bank_account", "bank_account",
"bank", "bank",
"column_break_4", "column_break_4",
"custom_delimiters",
"delimiter_options",
"google_sheets_url", "google_sheets_url",
"refresh_google_sheet", "refresh_google_sheet",
"html_5", "html_5",
@@ -24,7 +26,6 @@
"section_import_preview", "section_import_preview",
"import_preview", "import_preview",
"import_log_section", "import_log_section",
"statement_import_log",
"show_failed_logs", "show_failed_logs",
"import_log_preview", "import_log_preview",
"reference_doctype", "reference_doctype",
@@ -194,15 +195,23 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "statement_import_log", "default": "0",
"fieldtype": "Code", "fieldname": "custom_delimiters",
"label": "Statement Import Log", "fieldtype": "Check",
"options": "JSON" "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, "hide_toolbar": 1,
"links": [], "links": [],
"modified": "2022-09-07 11:11:40.293317", "modified": "2024-06-25 17:32:07.658250",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Statement Import", "name": "Bank Statement Import",

View File

@@ -31,13 +31,14 @@ class BankStatementImport(DataImport):
bank: DF.Link | None bank: DF.Link | None
bank_account: DF.Link bank_account: DF.Link
company: DF.Link company: DF.Link
custom_delimiters: DF.Check
delimiter_options: DF.Data | None
google_sheets_url: DF.Data | None google_sheets_url: DF.Data | None
import_file: DF.Attach | None import_file: DF.Attach | None
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"] import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
mute_emails: DF.Check mute_emails: DF.Check
reference_doctype: DF.Link reference_doctype: DF.Link
show_failed_logs: DF.Check show_failed_logs: DF.Check
statement_import_log: DF.Code | None
status: DF.Literal["Pending", "Success", "Partial Success", "Error"] status: DF.Literal["Pending", "Success", "Partial Success", "Error"]
submit_after_import: DF.Check submit_after_import: DF.Check
template_options: DF.Code | None template_options: DF.Code | None
@@ -120,6 +121,11 @@ def download_errored_template(data_import_name):
data_import.export_errored_rows() 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): def parse_data_from_template(raw_data):
data = [] data = []
@@ -241,6 +247,47 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
return True 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() @frappe.whitelist()
def upload_bank_statement(**args): def upload_bank_statement(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -82,7 +82,6 @@ class PaymentEntry(AccountsController):
self.set_exchange_rate() self.set_exchange_rate()
self.validate_mandatory() self.validate_mandatory()
self.validate_reference_documents() self.validate_reference_documents()
self.set_tax_withholding()
self.set_amounts() self.set_amounts()
self.validate_amounts() self.validate_amounts()
self.apply_taxes() self.apply_taxes()
@@ -96,6 +95,7 @@ class PaymentEntry(AccountsController):
self.validate_allocated_amount() self.validate_allocated_amount()
self.validate_paid_invoices() self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.set_tax_withholding()
self.set_status() self.set_status()
self.set_total_in_words() self.set_total_in_words()
@@ -756,9 +756,7 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount: if not self.apply_tax_withholding_amount:
return return
order_amount = self.get_order_net_total() net_total = self.calculate_tax_withholding_net_total()
net_total = flt(order_amount) + flt(self.unallocated_amount)
# Adding args as purchase invoice to get TDS amount # Adding args as purchase invoice to get TDS amount
args = frappe._dict( args = frappe._dict(
@@ -802,7 +800,26 @@ class PaymentEntry(AccountsController):
for d in to_remove: for d in to_remove:
self.remove(d) 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": if self.party_type == "Supplier":
doctype = "Purchase Order" doctype = "Purchase Order"
else: else:
@@ -810,11 +827,14 @@ class PaymentEntry(AccountsController):
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype] docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
tax_withholding_net_total = frappe.db.get_value( return frappe._dict(
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"] 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): def apply_taxes(self):
self.initialize_taxes() self.initialize_taxes()

View File

@@ -139,6 +139,7 @@ class PricingRule(Document):
self.validate_price_list_with_currency() self.validate_price_list_with_currency()
self.validate_dates() self.validate_dates()
self.validate_condition() self.validate_condition()
self.validate_mixed_with_recursion()
if not self.margin_type: if not self.margin_type:
self.margin_rate_or_amount = 0.0 self.margin_rate_or_amount = 0.0
@@ -308,6 +309,10 @@ class PricingRule(Document):
): ):
frappe.throw(_("Invalid condition expression")) 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"))
# -------------------------------------------------------------------------------- # --------------------------------------------------------------------------------

View File

@@ -1299,6 +1299,18 @@ class TestPricingRule(unittest.TestCase):
item_group_rule.delete() item_group_rule.delete()
item_code_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"] test_dependencies = ["Campaign"]

View File

@@ -146,6 +146,7 @@ class PromotionalScheme(Document):
self.validate_applicable_for() self.validate_applicable_for()
self.validate_pricing_rules() self.validate_pricing_rules()
self.validate_mixed_with_recursion()
def validate_applicable_for(self): def validate_applicable_for(self):
if self.applicable_for: if self.applicable_for:
@@ -163,7 +164,7 @@ class PromotionalScheme(Document):
docnames = [] docnames = []
# If user has changed applicable for # 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 return
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) 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) frappe.delete_doc("Pricing Rule", docname.name)
def on_update(self): def on_update(self):
self.validate()
pricing_rules = ( pricing_rules = (
frappe.get_all( frappe.get_all(
"Pricing Rule", "Pricing Rule",
@@ -188,6 +190,15 @@ class PromotionalScheme(Document):
) )
self.update_pricing_rules(pricing_rules) 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): def update_pricing_rules(self, pricing_rules):
rules = {} rules = {}
count = 0 count = 0

View File

@@ -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] [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): def make_promotional_scheme(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -109,7 +109,7 @@ def get_provisional_profit_loss(
): ):
provisional_profit_loss = {} provisional_profit_loss = {}
total_row = {} total_row = {}
if asset and (liability or equity): if asset:
total = total_row_total = 0 total = total_row_total = 0
currency = currency or frappe.get_cached_value("Company", company, "default_currency") currency = currency or frappe.get_cached_value("Company", company, "default_currency")
total_row = { total_row = {
@@ -122,14 +122,20 @@ def get_provisional_profit_loss(
for period in period_list: for period in period_list:
key = period if consolidated else period.key key = period if consolidated else period.key
total_assets = flt(asset[0].get(key))
if liability or equity:
effective_liability = 0.0 effective_liability = 0.0
if liability: if liability:
effective_liability += flt(liability[-2].get(key)) effective_liability += flt(liability[0].get(key))
if equity: if equity:
effective_liability += flt(equity[-2].get(key)) effective_liability += flt(equity[0].get(key))
provisional_profit_loss[key] = flt(asset[-2].get(key)) - effective_liability provisional_profit_loss[key] = total_assets - effective_liability
total_row[key] = effective_liability + provisional_profit_loss[key] else:
provisional_profit_loss[key] = total_assets
total_row[key] = provisional_profit_loss[key]
if provisional_profit_loss[key]: if provisional_profit_loss[key]:
has_value = True has_value = True

View File

@@ -2,10 +2,12 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy
from collections import OrderedDict from collections import OrderedDict
import frappe import frappe
from frappe import _, _dict from frappe import _, _dict
from frappe.query_builder import Criterion
from frappe.utils import cstr, getdate from frappe.utils import cstr, getdate
from erpnext import get_company_currency, get_default_company 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.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
# to cache translations
TRANSLATIONS = frappe._dict()
def execute(filters=None): def execute(filters=None):
if not filters: if not filters:
@@ -44,19 +43,11 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
update_translations()
res = get_result(filters, account_details) res = get_result(filters, account_details)
return columns, res return columns, res
def update_translations():
TRANSLATIONS.update(
dict(OPENING=_("Opening"), TOTAL=_("Total"), CLOSING_TOTAL=_("Closing (Opening + Total)"))
)
def validate_filters(filters, account_details): def validate_filters(filters, account_details):
if not filters.get("company"): if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company"))) frappe.throw(_("{0} is mandatory").format(_("Company")))
@@ -319,26 +310,31 @@ def get_accounts_with_children(accounts):
if not isinstance(accounts, list): if not isinstance(accounts, list):
accounts = [d.strip() for d in accounts.strip().split(",") if d] accounts = [d.strip() for d in accounts.strip().split(",") if d]
all_accounts = [] if not accounts:
for d in accounts: return
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))
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): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] 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 # Opening for filtered account
data.append(totals.opening) data.append(totals.opening)
@@ -387,9 +383,9 @@ def get_totals_dict():
) )
return _dict( return _dict(
opening=_get_debit_credit_dict(TRANSLATIONS.OPENING), opening=_get_debit_credit_dict(_("Opening")),
total=_get_debit_credit_dict(TRANSLATIONS.TOTAL), total=_get_debit_credit_dict(_("Total")),
closing=_get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL), closing=_get_debit_credit_dict(_("Closing (Opening + Total)")),
) )
@@ -402,17 +398,16 @@ def group_by_field(group_by):
return "voucher_no" return "voucher_no"
def initialize_gle_map(gl_entries, filters): def initialize_gle_map(gl_entries, filters, totals_dict):
gle_map = OrderedDict() gle_map = OrderedDict()
group_by = group_by_field(filters.get("group_by")) group_by = group_by_field(filters.get("group_by"))
for gle in gl_entries: 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 return gle_map
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
totals = get_totals_dict()
entries = [] entries = []
consolidated_gle = OrderedDict() consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get("group_by")) group_by = group_by_field(filters.get("group_by"))

View File

@@ -12,7 +12,7 @@ def execute(filters=None):
else: else:
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name") 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) 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) tax_withholding_category = tds_accounts.get(entry.account)
# or else the consolidated value from the voucher document # or else the consolidated value from the voucher document
if not tax_withholding_category: 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 # or else from the party default
if not tax_withholding_category: if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.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": if voucher_type == "Journal Entry":
# back calcalute total amount from rate and tax_amount # back calcalute total amount from rate and tax_amount
if rate: if rate:
total_amount = grand_total = base_total = tax_amount / (rate / 100) total_amount = grand_total = base_total = tax_amount / (rate / 100)
elif voucher_type == "Purchase Invoice": 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: 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: else:
total_amount += entry.credit 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": 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( row.update(
{ {
@@ -279,7 +281,6 @@ def get_tds_docs(filters):
journal_entries = [] journal_entries = []
tax_category_map = frappe._dict() tax_category_map = frappe._dict()
net_total_map = frappe._dict() net_total_map = frappe._dict()
frappe._dict()
journal_entry_party_map = frappe._dict() journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") 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: 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": if doctype == "Purchase Invoice":
value = [ value = [
entry.base_tax_withholding_net_total, 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] value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
else: else:
value = [entry.total_amount] * 3 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): def get_tax_rate_map(filters):

View File

@@ -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 () { frm.set_query("warehouse", "stock_items", function () {
return { return {
filters: { filters: {

View File

@@ -39,6 +39,7 @@ def validate_filters(filters):
def get_data(filters): def get_data(filters):
po = frappe.qb.DocType("Purchase Order") po = frappe.qb.DocType("Purchase Order")
po_item = frappe.qb.DocType("Purchase Order Item") po_item = frappe.qb.DocType("Purchase Order Item")
pi = frappe.qb.DocType("Purchase Invoice")
pi_item = frappe.qb.DocType("Purchase Invoice Item") pi_item = frappe.qb.DocType("Purchase Invoice Item")
query = ( query = (
@@ -46,6 +47,8 @@ def get_data(filters):
.from_(po_item) .from_(po_item)
.left_join(pi_item) .left_join(pi_item)
.on(pi_item.po_detail == po_item.name) .on(pi_item.po_detail == po_item.name)
.left_join(pi)
.on(pi.name == pi_item.parent)
.select( .select(
po.transaction_date.as_("date"), po.transaction_date.as_("date"),
po_item.schedule_date.as_("required_date"), po_item.schedule_date.as_("required_date"),
@@ -69,6 +72,7 @@ def get_data(filters):
po_item.name, po_item.name,
) )
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1)) .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
.where(pi.docstatus == 1)
.groupby(po_item.name) .groupby(po_item.name)
.orderby(po.transaction_date) .orderby(po.transaction_date)
) )

View File

@@ -28,7 +28,7 @@ class SellingController(StockController):
def validate(self): def validate(self):
super().validate() super().validate()
self.validate_items() 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_max_discount()
self.validate_selling_price() self.validate_selling_price()
self.set_qty_as_per_stock_uom() self.set_qty_as_per_stock_uom()

View File

@@ -94,7 +94,10 @@ status_map = {
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], ["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"], ["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"], ["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"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
], ],

View File

@@ -218,7 +218,7 @@ class StockController(AccountsController):
"do_not_submit": True if not via_landed_cost_voucher else False, "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.update_bundle_details(bundle_details, table_name, row)
self.create_serial_batch_bundle(bundle_details, row) self.create_serial_batch_bundle(bundle_details, row)

View File

@@ -156,12 +156,12 @@ class BOMCreator(Document):
amount = self.get_raw_material_cost() amount = self.get_raw_material_cost()
self.raw_material_cost = amount self.raw_material_cost = amount
def get_raw_material_cost(self, fg_reference_id=None, amount=0): def get_raw_material_cost(self, fg_item=None, amount=0):
if not fg_reference_id: if not fg_item:
fg_reference_id = self.name fg_item = self.item_code
for row in self.items: for row in self.items:
if row.fg_reference_id != fg_reference_id: if row.fg_item != fg_item:
continue continue
if not row.is_expandable: if not row.is_expandable:
@@ -183,7 +183,7 @@ class BOMCreator(Document):
else: else:
row.amount = 0.0 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)) row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor))
amount += flt(row.amount) amount += flt(row.amount)

View File

@@ -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_warehouse_field_in_asset_repair_consumed_item_doctype
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry 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.update_total_number_of_booked_depreciations
erpnext.patches.v15_0.do_not_use_batchwise_valuation

View File

@@ -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)

View File

@@ -262,8 +262,7 @@ class Project(Document):
if self.status == "Cancelled": if self.status == "Cancelled":
return return
if self.percent_complete == 100: self.status = "Completed" if self.percent_complete == 100 else "Open"
self.status = "Completed"
def update_costing(self): def update_costing(self):
from frappe.query_builder.functions import Max, Min, Sum from frappe.query_builder.functions import Max, Min, Sum

View File

@@ -1750,12 +1750,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
apply_product_discount(args) { apply_product_discount(args) {
const items = this.frm.doc.items.filter(d => (d.is_free_item)) || []; 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 => { args.free_item_data.forEach(pr_row => {
let row_to_modify = {}; 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, row_to_modify = frappe.model.add_child(this.frm.doc,
this.frm.doc.doctype + ' Item', 'items'); this.frm.doc.doctype + ' Item', 'items');

View File

@@ -220,6 +220,28 @@ erpnext.sales_common = {
if (doc.docstatus === 0 && doc.is_return && !doc.return_against) { if (doc.docstatus === 0 && doc.is_return && !doc.return_against) {
frappe.model.set_value(cdt, cdn, "incoming_rate", 0.0); 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() { toggle_editable_price_list_rate() {

View File

@@ -36,7 +36,6 @@ frappe.ui.form.on("Import Supplier Invoice", {
toggle_read_only_fields: function (frm) { toggle_read_only_fields: function (frm) {
if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) { if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) {
cur_frm.set_read_only(); cur_frm.set_read_only();
cur_frm.refresh_fields();
frm.set_df_property("import_invoices", "hidden", 1); frm.set_df_property("import_invoices", "hidden", 1);
} else { } else {
frm.set_df_property("import_invoices", "hidden", 0); frm.set_df_property("import_invoices", "hidden", 0);

View File

@@ -43,6 +43,7 @@ class HolidayList(Document):
self.validate_days() self.validate_days()
self.total_holidays = len(self.holidays) self.total_holidays = len(self.holidays)
self.validate_duplicate_date() self.validate_duplicate_date()
self.sort_holidays()
@frappe.whitelist() @frappe.whitelist()
def get_weekly_off_dates(self): 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.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
self.sort_holidays()
@frappe.whitelist() @frappe.whitelist()
def get_supported_countries(self): def get_supported_countries(self):
from holidays.utils import list_supported_countries 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} "holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
) )
self.sort_holidays()
def sort_holidays(self): def sort_holidays(self):
self.holidays.sort(key=lambda x: getdate(x.holiday_date)) self.holidays.sort(key=lambda x: getdate(x.holiday_date))
for i in range(len(self.holidays)): for i in range(len(self.holidays)):

View File

@@ -158,6 +158,10 @@ class Batch(Document):
def set_batchwise_valuation(self): def set_batchwise_valuation(self):
if self.is_new(): 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 self.use_batchwise_valuation = 1
def before_save(self): def before_save(self):

View File

@@ -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( this.frm.add_custom_button(
__("Delivery Trip"), __("Delivery Trip"),
function () { function () {

View File

@@ -1654,7 +1654,7 @@ class TestDeliveryNote(FrappeTestCase):
{ {
"is_stock_item": 1, "is_stock_item": 1,
"has_batch_no": 1, "has_batch_no": 1,
"batch_no_series": "BATCH-TESTSERIAL-.#####", "batch_number_series": "BATCH-TESTSERIAL-.#####",
"create_new_batch": 1, "create_new_batch": 1,
}, },
) )

View File

@@ -58,10 +58,12 @@ frappe.ui.form.on("Delivery Trip", {
date_field: "posting_date", date_field: "posting_date",
setters: { setters: {
company: frm.doc.company, company: frm.doc.company,
customer: null,
}, },
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
company: frm.doc.company, company: frm.doc.company,
status: ["Not In", ["Completed", "Cancelled"]],
}, },
}); });
}, },

View File

@@ -189,7 +189,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-05 12:52:18.705431", "modified": "2024-07-08 08:58:50.228211",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Inventory Dimension", "name": "Inventory Dimension",
@@ -221,16 +221,8 @@
"write": 1 "write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1, "read": 1,
"report": 1, "role": "Stock User"
"role": "Stock User",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -33,7 +33,7 @@ class InventoryDimension(Document):
dimension_name: DF.Data dimension_name: DF.Data
disabled: DF.Check disabled: DF.Check
document_type: DF.Link | None document_type: DF.Link | None
fetch_from_parent: DF.Literal fetch_from_parent: DF.Literal[None]
istable: DF.Check istable: DF.Check
mandatory_depends_on: DF.SmallText | None mandatory_depends_on: DF.SmallText | None
reference_document: DF.Link reference_document: DF.Link

View File

@@ -6,7 +6,7 @@ from collections import OrderedDict, defaultdict
from itertools import groupby from itertools import groupby
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
@@ -73,6 +73,9 @@ class PickList(Document):
def validate(self): def validate(self):
self.validate_for_qty() 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): def before_save(self):
self.update_status() self.update_status()
@@ -82,6 +85,60 @@ class PickList(Document):
if self.get("locations"): if self.get("locations"):
self.validate_sales_order_percentage() 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): def validate_sales_order_percentage(self):
# set percentage picked in SO # set percentage picked in SO
for location in self.get("locations"): for location in self.get("locations"):

View File

@@ -648,7 +648,7 @@ class TestPickList(FrappeTestCase):
def test_picklist_for_batch_item(self): def test_picklist_for_batch_item(self):
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
item = make_item( 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 ).name
# create batch # create batch
@@ -1132,3 +1132,45 @@ class TestPickList(FrappeTestCase):
pl.save() pl.save()
self.assertEqual(pl.locations[0].qty, 80.0) 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)

View File

@@ -1111,6 +1111,7 @@
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal",
"fieldname": "per_returned", "fieldname": "per_returned",
"fieldtype": "Percent", "fieldtype": "Percent",
"in_list_view": 1,
"label": "% Returned", "label": "% Returned",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
@@ -1272,7 +1273,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-04-08 20:23:03.699201", "modified": "2024-07-04 14:50:10.538472",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@@ -785,7 +785,6 @@ class PurchaseReceipt(BuyingController):
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): 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")]) 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 # Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {} valuation_tax = {}
for tax in self.get("taxes"): 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]) against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values()) total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked 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 i = 1
for tax in self.get("taxes"): for tax in self.get("taxes"):
if valuation_tax.get(tax.name): if valuation_tax.get(tax.name):
if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item():
account = tax.account_head 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
if i == len(valuation_tax): if i == len(valuation_tax):
applicable_amount = amount_including_divisional_loss applicable_amount = amount_including_divisional_loss
else: else:

View File

@@ -8,6 +8,7 @@ from pypika import functions as fn
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account 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.controllers.buying_controller import QtyMismatchError
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
@@ -3170,6 +3171,185 @@ class TestPurchaseReceipt(FrappeTestCase):
lcv.save().submit() lcv.save().submit()
return lcv 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(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -3412,7 +3592,6 @@ def make_purchase_receipt(**args):
pr.insert() pr.insert()
if not args.do_not_submit: if not args.do_not_submit:
pr.submit() pr.submit()
pr.load_from_db() pr.load_from_db()
return pr return pr

View File

@@ -111,10 +111,11 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-11-25 20:39:19.973437", "modified": "2024-07-08 09:19:26.711470",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Putaway Rule", "name": "Putaway Rule",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -130,16 +131,10 @@
"write": 1 "write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Stock User", "role": "Stock User"
"share": 1,
"write": 1
}, },
{ {
"email": 1, "email": 1,

View File

@@ -228,6 +228,16 @@ class SerialandBatchBundle(Document):
def get_serial_nos(self): def get_serial_nos(self):
return [d.serial_no for d in self.entries if d.serial_no] 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): def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False):
sle = self.get_sle_for_outward_transaction() sle = self.get_sle_for_outward_transaction()

View File

@@ -653,7 +653,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
"is_stock_item": 1, "is_stock_item": 1,
"has_batch_no": 1, "has_batch_no": 1,
"create_new_batch": 1, "create_new_batch": 1,
"batch_no_series": "PSNBI-TSNVL-.#####", "batch_number_series": "PSNBI-TSNVL-.#####",
"has_serial_no": 1, "has_serial_no": 1,
"serial_no_series": "SN-PSNBI-TSNVL-.#####", "serial_no_series": "SN-PSNBI-TSNVL-.#####",
}, },

View File

@@ -29,7 +29,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-05-21 11:27:01.144110", "modified": "2024-07-08 08:41:19.385020",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Type", "name": "Stock Entry Type",
@@ -72,16 +72,8 @@
"write": 1 "write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1, "read": 1,
"report": 1, "role": "Stock User"
"role": "Stock User",
"share": 1,
"write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@@ -44,6 +44,7 @@
"auto_reserve_serial_and_batch", "auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab", "serial_and_batch_item_settings_tab",
"section_break_7", "section_break_7",
"do_not_use_batchwise_valuation",
"auto_create_serial_and_batch_bundle_for_outward", "auto_create_serial_and_batch_bundle_for_outward",
"pick_serial_and_batch_based_on", "pick_serial_and_batch_based_on",
"column_break_mhzc", "column_break_mhzc",
@@ -437,6 +438,14 @@
"fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle", "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle" "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", "icon": "icon-cog",
@@ -444,7 +453,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-02-25 16:32:01.084453", "modified": "2024-07-04 12:45:09.811280",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -40,6 +40,7 @@ class StockSettings(Document):
default_warehouse: DF.Link | None default_warehouse: DF.Link | None
disable_serial_no_and_batch_selector: DF.Check disable_serial_no_and_batch_selector: DF.Check
do_not_update_serial_batch_on_creation_of_auto_bundle: 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 enable_stock_reservation: DF.Check
item_group: DF.Link | None item_group: DF.Link | None
item_naming_by: DF.Literal["Item Code", "Naming Series"] item_naming_by: DF.Literal["Item Code", "Naming Series"]
@@ -98,6 +99,22 @@ class StockSettings(Document):
self.validate_stock_reservation() self.validate_stock_reservation()
self.change_precision_for_for_sales() self.change_precision_for_for_sales()
self.change_precision_for_purchase() 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): def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]

View File

@@ -530,6 +530,10 @@ class update_entries_after:
self.allow_zero_rate = allow_zero_rate self.allow_zero_rate = allow_zero_rate
self.via_landed_cost_voucher = via_landed_cost_voucher self.via_landed_cost_voucher = via_landed_cost_voucher
self.item_code = args.get("item_code") 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( self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
item_code=self.item_code item_code=self.item_code
) )
@@ -745,7 +749,7 @@ class update_entries_after:
if sle.get(dimension.get("fieldname")): if sle.get(dimension.get("fieldname")):
has_dimensions = True 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) self.calculate_valuation_for_serial_batch_bundle(sle)
elif sle.serial_no and not self.args.get("sle_id"): elif sle.serial_no and not self.args.get("sle_id"):
# Only run in reposting # Only run in reposting
@@ -765,7 +769,12 @@ class update_entries_after:
# Only run in reposting # Only run in reposting
self.update_batched_values(sle) self.update_batched_values(sle)
else: 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 # assert
self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction self.wh_data.qty_after_transaction = sle.qty_after_transaction
@@ -806,6 +815,15 @@ class update_entries_after:
sle.doctype = "Stock Ledger Entry" sle.doctype = "Stock Ledger Entry"
frappe.get_doc(sle).db_update() 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 ( if not self.args.get("sle_id") or (
sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle 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 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): def validate_negative_stock(self, sle):
""" """
validate negative stock for entries current datetime onwards validate negative stock for entries current datetime onwards

View File

@@ -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 "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): if isinstance(args, dict):
args = frappe._dict(args) 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() 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 args.actual_qty = args.qty
batch_obj = BatchNoValuation( batch_obj = BatchNoValuation(
sle=args, 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")) sn_obj = SerialNoValuation(sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code"))
return sn_obj.get_incoming_rate() 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.actual_qty = args.qty
args.batch_nos = frappe._dict({args.batch_no: args}) args.batch_nos = frappe._dict({args.batch_no: args})

View File

@@ -1231,6 +1231,8 @@ Is Default,Ist Standard,
Is Existing Asset,Vermögenswert existiert bereits., Is Existing Asset,Vermögenswert existiert bereits.,
Is Frozen,Ist gesperrt, Is Frozen,Ist gesperrt,
Is Group,Ist Gruppe, Is Group,Ist Gruppe,
Is Group Warehouse,Ist Lagergruppe,
Is Rejected Warehouse,Ist Lager für abgelehnte Ware,
Issue,Anfrage, Issue,Anfrage,
Issue Material,Material ausgeben, Issue Material,Material ausgeben,
Issued,Ausgestellt, Issued,Ausgestellt,
@@ -1294,6 +1296,7 @@ Items for Raw Material Request,Artikel für Rohstoffanforderung,
Job Card,Jobkarte, Job Card,Jobkarte,
Job card {0} created,Jobkarte {0} erstellt, Job card {0} created,Jobkarte {0} erstellt,
Join,Beitreten, Join,Beitreten,
Joining,Beitritt,
Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft, Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft,
Journal Entry,Buchungssatz, 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, 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,Produktion,
Production Item,Produktions-Artikel, Production Item,Produktions-Artikel,
Products,Produkte, Products,Produkte,
Profile,Profil,
Profit and Loss,Gewinn und Verlust, Profit and Loss,Gewinn und Verlust,
Profit for the year,Jahresüberschuss, Profit for the year,Jahresüberschuss,
Program,Programm, Program,Programm,
@@ -2054,6 +2058,9 @@ Qty To Manufacture,Herzustellende Menge,
Qty Total,Gesamtmenge, Qty Total,Gesamtmenge,
Qty for {0},Menge für {0}, Qty for {0},Menge für {0},
Qualification,Qualifikation, Qualification,Qualifikation,
Qualification Status,Qualifikationsstatus,
Qualified By,Qualifiziert von,
Qualified on,Qualifiziert am,
Quality,Qualität, Quality,Qualität,
Quality Action,Qualitätsmaßnahme, Quality Action,Qualitätsmaßnahme,
Quality Goal.,Qualitätsziel., Quality Goal.,Qualitätsziel.,
@@ -3227,6 +3234,7 @@ Warehouse Type,Lagertyp,
'Date' is required,&#39;Datum&#39; ist erforderlich, 'Date' is required,&#39;Datum&#39; ist erforderlich,
Budgets,Budgets, Budgets,Budgets,
Bundle Qty,Bundle Menge, Bundle Qty,Bundle Menge,
Company Details,Unternehmensdetails,
Company GSTIN,Unternehmen GSTIN, Company GSTIN,Unternehmen GSTIN,
Company field is required,Firmenfeld ist erforderlich, Company field is required,Firmenfeld ist erforderlich,
Creating Dimensions...,Dimensionen erstellen ..., Creating Dimensions...,Dimensionen erstellen ...,
@@ -3562,6 +3570,7 @@ Performance,Performance,
Period based On,Zeitraum basierend auf, 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.", 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,Telefon,
Phone Ext.,Telefon Ext.,
Pick List,Auswahlliste, Pick List,Auswahlliste,
Plaid authentication error,Plaid-Authentifizierungsfehler, Plaid authentication error,Plaid-Authentifizierungsfehler,
Plaid public token error,Plaid public token error, Plaid public token error,Plaid public token error,
@@ -4970,7 +4979,7 @@ Number of Depreciations Booked,Anzahl der gebuchten Abschreibungen,
Finance Books,Finanzbücher, Finance Books,Finanzbücher,
Straight Line,Gerade Linie, Straight Line,Gerade Linie,
Double Declining Balance,Doppelte degressive, Double Declining Balance,Doppelte degressive,
Manual,Handbuch, Manual,Manuell,
Value After Depreciation,Wert nach Abschreibung, Value After Depreciation,Wert nach Abschreibung,
Total Number of Depreciations,Gesamtzahl der Abschreibungen, Total Number of Depreciations,Gesamtzahl der Abschreibungen,
Frequency of Depreciation (Months),Die Häufigkeit der Abschreibungen (Monate), 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, Maintenance Team Members,Mitglieder des Wartungsteams,
Purpose,Zweck, Purpose,Zweck,
Stock Manager,Lagerleiter, Stock Manager,Lagerleiter,
Stock Movement,Lagerbewegung,
Asset Movement Item,Vermögensbewegungsgegenstand, Asset Movement Item,Vermögensbewegungsgegenstand,
Source Location,Quellspeicherort, Source Location,Quellspeicherort,
From Employee,Von Mitarbeiter, From Employee,Von Mitarbeiter,
@@ -5126,6 +5136,7 @@ Default Bank Account,Standardbankkonto,
Is Transporter,Ist Transporter, Is Transporter,Ist Transporter,
Represents Company,Repräsentiert das Unternehmen, Represents Company,Repräsentiert das Unternehmen,
Supplier Type,Lieferantentyp, 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 Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen,
Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Eingangsbeleg zulassen, Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Eingangsbeleg zulassen,
Warn RFQs,Warnung Ausschreibungen, Warn RFQs,Warnung Ausschreibungen,
@@ -5899,6 +5910,7 @@ Occupational Hazards and Environmental Factors,Berufsrisiken und Umweltfaktoren,
Other Risk Factors,Andere Risikofaktoren, Other Risk Factors,Andere Risikofaktoren,
Patient Details,Patientendetails, Patient Details,Patientendetails,
Additional information regarding the patient,Zusätzliche Informationen zum Patienten, Additional information regarding the patient,Zusätzliche Informationen zum Patienten,
Additional Info,Zusätzliche Informationen,
HLC-APP-.YYYY.-,HLC-APP-.YYYY.-, HLC-APP-.YYYY.-,HLC-APP-.YYYY.-,
Patient Age,Patient Alter, Patient Age,Patient Alter,
Get Prescribed Clinical Procedures,Holen Sie sich vorgeschriebene klinische Verfahren, 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, Department and Grade,Abteilung und Klasse,
Reports to,Vorgesetzter, Reports to,Vorgesetzter,
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, 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), Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
Applicable Holiday List,Geltende Feiertagsliste, Applicable Holiday List,Geltende Feiertagsliste,
Default Shift,Standardverschiebung, Default Shift,Standardverschiebung,
@@ -6519,7 +6532,7 @@ Default Costing Rate,Standardkosten,
Default Billing Rate,Standard-Rechnungspreis, Default Billing Rate,Standard-Rechnungspreis,
Dependent Task,Abhängiger Vorgang, Dependent Task,Abhängiger Vorgang,
Project Type,Projekttyp, Project Type,Projekttyp,
% Complete Method,% abgeschlossene Methode, % Complete Method,Fertigstellung bemessen nach,
Task Completion,Aufgabenerledigung, Task Completion,Aufgabenerledigung,
Task Progress,Vorgangsentwicklung, Task Progress,Vorgangsentwicklung,
% Completed,% abgeschlossen, % Completed,% abgeschlossen,
@@ -6675,6 +6688,7 @@ Restaurant Reservation,Restaurant Reservierung,
Waitlisted,Auf der Warteliste, Waitlisted,Auf der Warteliste,
No Show,Nicht angetreten, No Show,Nicht angetreten,
No of People,Anzahl von Personen, No of People,Anzahl von Personen,
No of Employees,Anzahl der Mitarbeiter,
Reservation Time,Reservierungszeit, Reservation Time,Reservierungszeit,
Reservation End Time,Reservierungsendzeit, Reservation End Time,Reservierungsendzeit,
No of Seats,Anzahl der Sitze, No of Seats,Anzahl der Sitze,
@@ -6686,6 +6700,7 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens,
From Lead,Aus Lead, From Lead,Aus Lead,
Account Manager,Kundenberater, Account Manager,Kundenberater,
Accounts Manager,Buchhalter, 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 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, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein,
Default Price List,Standardpreisliste, Default Price List,Standardpreisliste,
@@ -6745,7 +6760,8 @@ Not Delivered,Nicht geliefert,
Fully Delivered,Komplett geliefert, Fully Delivered,Komplett geliefert,
Partly Delivered,Teilweise geliefert, Partly Delivered,Teilweise geliefert,
Not Applicable,Nicht andwendbar, 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 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, % of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden,
Not Billed,Nicht abgerechnet, Not Billed,Nicht abgerechnet,
@@ -6879,6 +6895,7 @@ New Income,Neuer Verdienst,
New Expenses,Neue Ausgaben, New Expenses,Neue Ausgaben,
Annual Income,Jährliches Einkommen, Annual Income,Jährliches Einkommen,
Annual Expenses,Jährliche Kosten, Annual Expenses,Jährliche Kosten,
Annual Revenue,Jährlicher Umsatz,
Bank Balance,Kontostand, Bank Balance,Kontostand,
Bank Credit Balance,Bankguthaben, Bank Credit Balance,Bankguthaben,
Receivables,Forderungen, Receivables,Forderungen,
@@ -7352,6 +7369,7 @@ Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen,
Stock Value Difference,Lagerwert-Differenz, Stock Value Difference,Lagerwert-Differenz,
Stock Queue (FIFO),Lagerverfahren (FIFO), Stock Queue (FIFO),Lagerverfahren (FIFO),
Is Cancelled,Ist storniert, Is Cancelled,Ist storniert,
Is Cash or Non Trade Discount,Ist Bar- oder Nicht-Handelsrabatt,
Stock Reconciliation,Bestandsabgleich, 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.", 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), 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, Resolved By,Entschieden von,
Service Address,Serviceadresse, Service Address,Serviceadresse,
If different than customer address,Falls abweichend von Kundenadresse, 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, Raised By,Gemeldet durch,
From Company,Von Unternehmen, From Company,Von Unternehmen,
Rename Tool,Werkzeug zum Umbenennen, Rename Tool,Werkzeug zum Umbenennen,
@@ -8628,6 +8647,7 @@ Print Receipt,Druckeingang,
Edit Receipt,Beleg bearbeiten, Edit Receipt,Beleg bearbeiten,
Focus on search input,Konzentrieren Sie sich auf die Sucheingabe, Focus on search input,Konzentrieren Sie sich auf die Sucheingabe,
Focus on Item Group filter,Fokus auf Artikelgruppenfilter, 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, Checkout Order / Submit Order / New Order,Kaufabwicklung / Bestellung abschicken / Neue Bestellung,
Add Order Discount,Bestellrabatt hinzufügen, Add Order Discount,Bestellrabatt hinzufügen,
Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar., Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar.,
Can't render this file because it is too large.