Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-46223

This commit is contained in:
Mihir Kandoi
2025-03-05 12:51:01 +05:30
committed by GitHub
42 changed files with 406 additions and 68 deletions

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, throw
from frappe.utils import cint, cstr
from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
@@ -481,6 +481,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
if not account:
return
@@ -542,6 +543,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -595,3 +597,27 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)
def _ensure_idle_system():
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
if frappe.flags.in_test:
return
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
if last_gl_update > add_to_date(None, minutes=-5):
frappe.throw(
_(
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)

View File

@@ -1843,7 +1843,7 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"):
elif self.party_type in ("Supplier", "Customer"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
@@ -3337,13 +3337,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(base_loss_on_income, precision),
"amount": flt(base_loss_on_income, precision) * positive_negative,
},
)
@@ -3355,6 +3356,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
tax_discount_loss = {}
base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
positive_negative = -1 if pe.payment_type == "Pay" else 1
# The same account head could be used more than once
for tax in doc.get("taxes", []):
@@ -3377,7 +3379,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"account": account,
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(loss, precision),
"amount": flt(loss, precision) * positive_negative,
},
)

View File

@@ -282,6 +282,48 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
pi = make_purchase_invoice(do_not_save=1)
create_payment_terms_template_with_discount()
pi.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
pi.save()
pi.submit()
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
self.assertEqual(pe_with_tax_loss.difference_amount, 0)
self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe.payment_type, "Pay")
self.assertEqual(pe.references[0].allocated_amount, 295.0)
self.assertEqual(pe.paid_amount, 265.5)
self.assertEqual(pe.deductions[0].amount, -29.5)
self.assertEqual(pe.difference_amount, 0)
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()

View File

@@ -1623,6 +1623,5 @@
"states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
"track_changes": 1
}

View File

@@ -13,17 +13,15 @@
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Type",
"options": "DocType"
"label": "Voucher Type"
},
{
"fieldname": "voucher_name",
"fieldtype": "Dynamic Link",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Name",
"options": "voucher_type"
"label": "Voucher Name"
},
{
"fieldname": "taxable_amount",
@@ -36,7 +34,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-01-13 13:40:41.479208",
"modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",

View File

@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
parentfield: DF.Data
parenttype: DF.Data
taxable_amount: DF.Currency
voucher_name: DF.DynamicLink | None
voucher_type: DF.Link | None
voucher_name: DF.Data | None
voucher_type: DF.Data | None
# end: auto-generated types
pass

View File

@@ -436,6 +436,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"),
company,
),
as_dict=1,
)
for d in journal_entries_details:

View File

@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD")
def test_account_payable_for_debit_note(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.is_return = 1
pi.items[0].qty = -1
pi = pi.save().submit()
filters = {
"company": self.company,
"party_type": "Supplier",
"party": [self.supplier],
"report_date": today(),
"range": "30, 60, 90, 120",
}
data = execute(filters)
self.assertEqual(data[1][0].get("invoiced"), 300)
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")
pi = make_purchase_invoice(

View File

@@ -267,6 +267,18 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
# when invoice has is_return marked
if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
# for Credit Note
if row.voucher_type == "Sales Invoice":
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
# for Debit Note
else:
row.invoiced -= amount
row.invoiced_in_account_currency -= amount_in_account_currency
return
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
# nosemgrep
si_list = frappe.db.sql(
"""
select name, due_date, po_no
select name, due_date, po_no, is_return
from `tabSales Invoice`
where posting_date <= %s
and company = %s
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
# nosemgrep
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
select name, due_date, bill_no, bill_date, is_return
from `tabPurchase Invoice`
where
posting_date <= %s

View File

@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
[0, 0, 0, 100.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = next(
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
[
row.invoiced or row.paid,
row.credit_note,
row.outstanding,
row.remaining_balance,
row.future_amount,
],
)
pe.cancel()

View File

@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0],
["Purchase Invoice", "is_opening", "!=", "Yes"],
]
if report_filters.get("purchase_invoice"):

View File

@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
and ba.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
},
{
fieldname: "project",
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
{
fieldname: "name",
label: __("Purchase Order"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
width: "80",
options: "Purchase Order",
get_query: () => {
return {
filters: { docstatus: 1 },
};
get_data: function (txt) {
let filters = { docstatus: 1 };
const from_date = frappe.query_report.get_filter_value("from_date");
const to_date = frappe.query_report.get_filter_value("to_date");
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
return frappe.db.get_link_options("Purchase Order", txt, filters);
},
},
{
@@ -52,9 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
label: __("Status"),
fieldtype: "MultiSelectList",
width: "80",
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
get_data: function (txt) {
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
let status = [
"To Pay",
"To Bill",
"To Receive",
"To Receive and Bill",
"Completed",
"Closed",
];
let options = [];
for (let option of status) {
options.push({

View File

@@ -70,14 +70,16 @@ def get_data(filters):
po.company,
po_item.name,
)
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
.groupby(po_item.name)
.orderby(po.transaction_date)
)
for field in ("company", "name"):
if filters.get(field):
query = query.where(po[field] == filters.get(field))
if filters.get("company"):
query = query.where(po.company == filters.get("company"))
if filters.get("name"):
query = query.where(po.name.isin(filters.get("name")))
if filters.get("from_date") and filters.get("to_date"):
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))

View File

@@ -194,6 +194,14 @@ class AccountsController(TransactionBase):
self.set_incoming_rate()
self.init_internal_values()
# Need to set taxes based on taxes_and_charges template
# before calculating taxes and totals
if self.meta.get_field("taxes_and_charges"):
self.validate_enabled_taxes_and_charges()
self.validate_tax_account_company()
self.set_taxes_and_charges()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -204,10 +212,6 @@ class AccountsController(TransactionBase):
self.validate_all_documents_schedule()
if self.meta.get_field("taxes_and_charges"):
self.validate_enabled_taxes_and_charges()
self.validate_tax_account_company()
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
@@ -252,8 +256,6 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
self.set_taxes_and_charges()
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -821,11 +823,15 @@ class AccountsController(TransactionBase):
and item.get("use_serial_batch_fields")
)
):
if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
item.set("rate", ret.get("rate"))
item.set("price_list_rate", ret.get("price_list_rate"))
item.set(fieldname, value)
if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
if ret.get("rate"):
item.set("rate", ret.get("rate"))
if not item.get("price_list_rate") and ret.get("price_list_rate"):
item.set("price_list_rate", ret.get("price_list_rate"))
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
fieldname
):

View File

@@ -931,6 +931,35 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
@change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
# Create a Sales Taxes and Charges Template
if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
doc = frappe.new_doc("Sales Taxes and Charges Template")
doc.company = self.company
doc.title = "_Test Tax"
doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "Sales Expenses - _TC",
"description": "Test taxes",
"rate": 9,
},
)
doc.insert()
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = self.customer
sinv.company = self.company
sinv.currency = "INR"
sinv.taxes_and_charges = "_Test Tax - _TC"
sinv.append("items", {"item_code": "_Test Item", "qty": 1, "rate": 50})
sinv.insert()
self.assertEqual(sinv.total_taxes_and_charges, 4.5)
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

View File

@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = """ERP made simple"""
app_icon = "fa fa-th"
app_color = "#e74c3c"
app_email = "info@erpnext.com"
app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -484,7 +484,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """
<span>
Sent via
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
ERPNext
</a>
</span>

View File

@@ -28,6 +28,8 @@ BOM_ITEM_FIELDS = [
"stock_uom",
"conversion_factor",
"do_not_explode",
"source_warehouse",
"allow_alternative_item",
]
@@ -291,7 +293,6 @@ class BOMCreator(Document):
"item": row.item_code,
"bom_type": "Production",
"quantity": row.qty,
"allow_alternative_item": 1,
"bom_creator": self.name,
"bom_creator_item": bom_creator_item,
}
@@ -315,7 +316,6 @@ class BOMCreator(Document):
item_args.update(
{
"bom_no": bom_no,
"allow_alternative_item": 1,
"allow_scrap_items": 1,
"include_item_in_manufacturing": 1,
}
@@ -428,6 +428,7 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1,
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
"allow_alternative_item": kwargs.allow_alternative_item,
},
)

View File

@@ -15,6 +15,7 @@
"is_expandable",
"sourced_by_supplier",
"bom_created",
"allow_alternative_item",
"description_section",
"description",
"quantity_and_rate_section",
@@ -225,12 +226,18 @@
"label": "BOM Created",
"no_copy": 1,
"print_hide": 1
},
{
"default": "1",
"fieldname": "allow_alternative_item",
"fieldtype": "Check",
"label": "Allow Alternative Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-06-03 18:45:24.339532",
"modified": "2025-02-19 13:25:15.732496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",

View File

@@ -14,6 +14,7 @@ class BOMCreatorItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
allow_alternative_item: DF.Check
amount: DF.Currency
base_amount: DF.Currency
base_rate: DF.Currency

View File

@@ -394,3 +394,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_post
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes

View File

@@ -82,6 +82,9 @@ def get_asset_depreciation_schedules_map():
.orderby(ds.idx)
).run(as_dict=True)
if len(records) > 20000:
frappe.db.auto_commit_on_many_writes = True
asset_depreciation_schedules_map = frappe._dict()
for d in records:
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)

View File

@@ -210,6 +210,13 @@ class BOMConfigurator {
[
{ label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
{ label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
{
label: __("Allow Alternative Item"),
fieldname: "allow_alternative_item",
default: 1.0,
fieldtype: "Check",
reqd: 1,
},
],
(data) => {
if (!node.data.parent_id) {
@@ -224,6 +231,7 @@ class BOMConfigurator {
item_code: data.item_code,
fg_reference_id: node.data.name || this.frm.doc.name,
qty: data.qty,
allow_alternative_item: data.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -258,6 +266,7 @@ class BOMConfigurator {
fg_item: node.data.value,
fg_reference_id: node.data.name || this.frm.doc.name,
bom_item: bom_item,
allow_alternative_item: bom_item.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -278,6 +287,14 @@ class BOMConfigurator {
reqd: 1,
read_only: read_only,
},
{
label: __("Allow Alternative Item"),
fieldname: "allow_alternative_item",
default: 1.0,
fieldtype: "Check",
reqd: 1,
read_only: read_only,
},
{ fieldtype: "Column Break" },
{
label: __("Qty"),

View File

@@ -14,6 +14,10 @@ erpnext.landed_cost_taxes_and_charges = {
"Income Account",
"Expenses Included In Valuation",
"Expenses Included In Asset Valuation",
"Expense Account",
"Direct Expense",
"Indirect Expense",
"Stock Received But Not Billed",
],
],
company: frm.doc.company,

View File

@@ -540,6 +540,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
has_batch_no: this.item.has_batch_no,
qty: qty,
based_on: based_on,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
},
callback: (r) => {
if (r.message) {

View File

@@ -1,4 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
{% if country != "United States" %}{{ country }}{% endif -%}

View File

@@ -2097,6 +2097,45 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def test_delivery_note_rate_on_change_of_warehouse(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item = make_item(
"_Test Batch Item for Delivery Note Rate",
{
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BH-SDDTBIFRM-.#####",
},
)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
so = make_sales_order(
item_code=item.name, rate=27648.00, price_list_rate=27648.00, qty=1, do_not_submit=True
)
so.items[0].rate = 90
so.save()
self.assertTrue(so.items[0].discount_amount == 27558.0)
so.submit()
warehouse = create_warehouse("NW Warehouse FOR Rate", company=so.company)
make_stock_entry(
item_code=item.name,
qty=2,
target=warehouse,
basic_rate=100,
company=so.company,
use_serial_batch_fields=1,
)
dn = make_delivery_note(so.name)
dn.items[0].warehouse = warehouse
dn.save()
self.assertEqual(dn.items[0].rate, 90)
def test_credit_limit_on_so_reopning(self):
# set credit limit
company = "_Test Company"

View File

@@ -320,13 +320,13 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
invoices_by_customer = frappe.db.get_all(
invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields,
page_length=limit,
)
invoices_by_name = frappe.db.get_all(
invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields,
@@ -335,7 +335,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name
elif status:
invoice_list = frappe.db.get_all(
invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
)

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
on_change: (report) => {
report.set_filter_value("sales_order", []);
report.refresh();
},
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("sales_order", []);
report.refresh();
},
},
{
fieldname: "sales_order",
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
options: "Sales Order",
get_data: function (txt) {
return frappe.db.get_link_options("Sales Order", txt);
},
get_query: () => {
return {
filters: { docstatus: 1 },
};
let filters = { docstatus: 1 };
const from_date = frappe.query_report.get_filter_value("from_date");
const to_date = frappe.query_report.get_filter_value("to_date");
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
return frappe.db.get_link_options("Sales Order", txt, filters);
},
},
{
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status",
label: __("Status"),
fieldtype: "MultiSelectList",
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed", "Closed"],
width: "80",
get_data: function (txt) {
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"];
let status = [
"To Pay",
"To Bill",
"To Deliver",
"To Deliver and Bill",
"Completed",
"Closed",
];
let options = [];
for (let option of status) {
options.push({

View File

@@ -86,7 +86,7 @@ def get_data(conditions, filters):
ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold')
and so.status not in ('Stopped', 'On Hold')
and so.docstatus = 1
{conditions}
GROUP BY soi.name

View File

@@ -16,7 +16,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
<a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
def after_install():

View File

@@ -1048,15 +1048,19 @@ def get_billed_amount_against_po(po_items):
if not po_items:
return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice_item.docstatus == 1)
& (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
& (purchase_invoice.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)

View File

@@ -4047,6 +4047,36 @@ class TestPurchaseReceipt(FrappeTestCase):
batch_return.save()
batch_return.submit()
def test_pr_status_based_on_invoices_with_update_stock(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as _make_purchase_invoice,
)
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt as _make_purchase_receipt,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
item_code = "Test Item for PR Status Based on Invoices"
create_item(item_code)
po = create_purchase_order(item_code=item_code, qty=10)
pi = _make_purchase_invoice(po.name)
pi.update_stock = 1
pi.items[0].qty = 5
pi.submit()
po.reload()
self.assertEqual(po.per_billed, 50)
pr = _make_purchase_receipt(po.name)
self.assertEqual(pr.items[0].qty, 5)
pr.submit()
pr.reload()
self.assertEqual(pr.status, "To Bill")
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -0,0 +1,9 @@
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
on_doctype_update as create_sle_indexes,
)
def execute():
"""Ensure SLE Indexes"""
create_sle_indexes()

View File

@@ -51,6 +51,10 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
latest_age = date_diff(to_date, fifo_queue[-1][1])
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
check_and_replace_valuations_if_moving_average(
range_values, details.valuation_method, details.valuation_rate
)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
if filters.get("show_warehouse_wise_stock"):
@@ -72,6 +76,15 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
return data
def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate):
if item_valuation_method == "Moving Average" or (
not item_valuation_method
and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average"
):
for i in range(0, len(range_values), 2):
range_values[i + 1] = range_values[i] * valuation_rate
def get_average_age(fifo_queue: list, to_date: str) -> float:
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
@@ -267,7 +280,7 @@ class FIFOSlots:
self.__update_balances(d, key)
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list
del stock_ledger_entries
if not self.filters.get("show_warehouse_wise_stock"):
@@ -396,6 +409,7 @@ class FIFOSlots:
self.item_details[key]["total_qty"] += row.actual_qty
self.item_details[key]["has_serial_no"] = row.has_serial_no
self.item_details[key]["details"].valuation_rate = row.valuation_rate
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
"Aggregate Item-Wh wise data into single Item entry."
@@ -437,8 +451,10 @@ class FIFOSlots:
item.description,
item.stock_uom,
item.has_serial_no,
item.valuation_method,
sle.actual_qty,
sle.stock_value_difference,
sle.valuation_rate,
sle.posting_date,
sle.voucher_type,
sle.voucher_no,
@@ -506,7 +522,14 @@ class FIFOSlots:
item_table = frappe.qb.DocType("Item")
item = frappe.qb.from_("Item").select(
"name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no"
"name",
"item_name",
"description",
"stock_uom",
"brand",
"item_group",
"has_serial_no",
"valuation_method",
)
if self.filters.get("item_code"):

View File

@@ -1006,6 +1006,10 @@ class SerialBatchCreation:
elif self.has_serial_no and not self.get("serial_nos"):
self.serial_nos = get_serial_nos_for_outward(kwargs)
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
if self.get("posting_date"):
kwargs["posting_date"] = self.get("posting_date")
kwargs["posting_time"] = self.get("posting_time")
self.batches = get_available_batches(kwargs)
def set_auto_serial_batch_entries_for_inward(self):

View File

@@ -561,12 +561,28 @@ class update_entries_after:
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.affected_transactions: set[tuple[str, str]] = set()
self.reserved_stock = flt(self.args.reserved_stock)
self.reserved_stock = self.get_reserved_stock()
self.data = frappe._dict()
self.initialize_previous_data(self.args)
self.build()
def get_reserved_stock(self):
sre = frappe.qb.DocType("Stock Reservation Entry")
posting_datetime = get_combine_datetime(self.args.posting_date, self.args.posting_time)
query = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty) - Sum(sre.delivered_qty))
.where(
(sre.item_code == self.item_code)
& (sre.warehouse == self.args.warehouse)
& (sre.docstatus == 1)
& (sre.creation <= posting_datetime)
)
).run()
return flt(query[0][0]) if query else 0.0
def set_precision(self):
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
self.currency_precision = get_field_precision(

View File

@@ -1 +1 @@
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}

View File

@@ -5098,7 +5098,7 @@ Percentage you are allowed to transfer more against the quantity ordered. For ex
PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-,
Get Items from Open Material Requests,Hole Artikel von offenen Material Anfragen,
Fetch items based on Default Supplier.,Abrufen von Elementen basierend auf dem Standardlieferanten.,
Required By,Benötigt von,
Required By,Benötigt bis,
Order Confirmation No,Auftragsbestätigung Nr,
Order Confirmation Date,Auftragsbestätigungsdatum,
Customer Mobile No,Mobilnummer des Kunden,
Can't render this file because it is too large.

View File

@@ -5,7 +5,7 @@
"type": "git",
"url": "git+https://github.com/frappe/erpnext.git"
},
"homepage": "https://erpnext.com",
"homepage": "https://frappe.io/erpnext",
"author": "Frappe Technologies Pvt. Ltd.",
"license": "GPL-3.0",
"bugs": {

View File

@@ -70,6 +70,6 @@ docstring-code-format = true
[project.urls]
Homepage = "https://erpnext.com/"
Homepage = "https://frappe.io/erpnext"
Repository = "https://github.com/frappe/erpnext.git"
"Bug Reports" = "https://github.com/frappe/erpnext/issues"