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,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"));
}
},
});
},
});

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -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"]

View File

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

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

View File

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

View File

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

View File

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

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 () {
return {
filters: {

View File

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

View File

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

View File

@@ -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"],
],

View File

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

View File

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

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_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

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":
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

View File

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

View File

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

View File

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

View File

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

View File

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

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(
__("Delivery Trip"),
function () {

View File

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

View File

@@ -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"]],
},
});
},

View File

@@ -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": []
}
}

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

@@ -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-.#####",
},

View File

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

View File

@@ -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",

View File

@@ -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"]

View File

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

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

View File

@@ -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,&#39;Datum&#39; 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.