mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 01:28:29 +00:00
Merge pull request #42253 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
} else {
|
||||
html = __("Successfully updated {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = log.messages
|
||||
.map(JSON.parse)
|
||||
.map((m) => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : "";
|
||||
let message = m.message ? `<div>${m.message}</div>` : "";
|
||||
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}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
} else {
|
||||
html = __("Successfully updated {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = JSON.parse(log.messages || "[]")
|
||||
.map((m) => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : "";
|
||||
let message = m.message ? `<div>${m.message}</div>` : "";
|
||||
return title + message;
|
||||
})
|
||||
.join("");
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__("Show Traceback")}
|
||||
</button>
|
||||
@@ -452,16 +476,16 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
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 `<tr>
|
||||
<td>${log.row_indexes.join(", ")}</td>
|
||||
return `<tr>
|
||||
<td>${JSON.parse(log.row_indexes).join(", ")}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
@@ -469,16 +493,16 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
})
|
||||
.join("");
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__("No failed logs")}
|
||||
</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
frm.get_field("import_log_preview").$wrapper.html(`
|
||||
frm.get_field("import_log_preview").$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__("Row Number")}</th>
|
||||
@@ -488,5 +512,34 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
${rows}
|
||||
</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"));
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"],
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
15
erpnext/patches/v15_0/do_not_use_batchwise_valuation.py
Normal file
15
erpnext/patches/v15_0/do_not_use_batchwise_valuation.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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"]],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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-.#####",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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.,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user