chore: release v13 (#37245)

* feat: provision to make reposting entries from Stock and Account Value Comparison Report (backport #35365) (#37171)

* feat: provision to make reposting entries from Stock and Account Value Comparison Report

(cherry picked from commit 7b818e9d34)

* fix: `linter`

* fix(ux): throw if no row selected to create repost entries

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* fix: incorrect stock ledger entries in DN (backport #36944) (#37067)

* fix: incorrect stock ledger entries in DN (#36944)

(cherry picked from commit 0e83190c19)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>

* feat: `Stock Ledger Variance` report (backport #37165) (#37184)

feat: `Stock Ledger Variance` report (#37165)

* feat: `Stock Ledger Variance` report

* refactor: `get_data()`

(cherry picked from commit acda72d616)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* fix: Update `advance_paid` in SO/PO after unlinking from advance entry

(cherry picked from commit 426350eee6)

* test: Impact on SO of advance PE submit and unlinking/replacement by SI

(cherry picked from commit 8a4954d713)

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
Co-authored-by: marination <maricadsouza221197@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
This commit is contained in:
Frappe PR Bot
2023-09-27 09:25:47 +05:30
committed by GitHub
parent 7c1288f726
commit 4285bbcdc0
9 changed files with 516 additions and 7 deletions

View File

@@ -1783,6 +1783,10 @@ class TestSalesInvoice(unittest.TestCase):
)
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
"""Test impact of advance PE submission/cancellation on SI and SO."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -1802,10 +1806,25 @@ class TestSalesInvoice(unittest.TestCase):
"paid_to": "_Test Cash - _TC",
}
)
pe.append(
"references",
{
"reference_doctype": "Sales Order",
"reference_name": sales_order.name,
"total_amount": sales_order.grand_total,
"outstanding_amount": sales_order.grand_total,
"allocated_amount": 300,
},
)
pe.insert()
pe.submit()
sales_order.reload()
self.assertEqual(sales_order.advance_paid, 300)
si = frappe.copy_doc(test_records[0])
si.items[0].sales_order = sales_order.name
si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0
si.append(
"advances",
@@ -1813,6 +1832,7 @@ class TestSalesInvoice(unittest.TestCase):
"doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry",
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 300,
"allocated_amount": 300,
"remarks": pe.remarks,
@@ -1821,7 +1841,13 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
si.submit()
si.load_from_db()
si.reload()
pe.reload()
sales_order.reload()
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(sales_order.advance_paid, 0.0)
# check outstanding after advance allocation
self.assertEqual(
@@ -1829,11 +1855,9 @@ class TestSalesInvoice(unittest.TestCase):
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
)
# added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
si.reload()
si.load_from_db()
# check outstanding after advance cancellation
self.assertEqual(
flt(si.outstanding_amount),

View File

@@ -537,6 +537,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -596,6 +600,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()
original_row = existing_row.as_dict().copy()
existing_row.update(reference_details)

View File

@@ -1268,6 +1268,7 @@
"depends_on": "eval: doc.is_internal_customer",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_standard_filter": 1,
"label": "Set Target Warehouse",
"no_copy": 1,
@@ -1335,7 +1336,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2022-09-16 17:46:17.701904",
"modified": "2023-09-04 14:15:28.363184",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
@@ -1405,4 +1406,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -33,5 +33,43 @@ frappe.query_reports["Stock and Account Value Comparison"] = {
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
},
]
],
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__("Create Reposting Entries"), function() {
let message = `
<div>
<p>
Reposting Entries will change the value of
accounts Stock In Hand, and Stock Expenses
in the Trial Balance report and will also change
the Balance Value in the Stock Balance report.
</p>
<p>Are you sure you want to create Reposting Entries?</p>
</div>`;
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select rows to create Reposting Entries"));
}
frappe.confirm(__(message), () => {
frappe.call({
method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries",
args: {
rows: selected_rows,
company: frappe.query_report.get_filter_values().company
}
});
});
});
}
};

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
import erpnext
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
@@ -134,3 +135,35 @@ def get_columns(filters):
"width": "120",
},
]
@frappe.whitelist()
def create_reposting_entries(rows, company):
if isinstance(rows, str):
rows = parse_json(rows)
entries = []
for row in rows:
row = frappe._dict(row)
try:
doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Transaction",
"status": "Queued",
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"posting_date": row.posting_date,
"company": company,
"allow_nagative_stock": 1,
}
).submit()
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
except frappe.DuplicateEntryError:
pass
if entries:
entries = ", ".join(entries)
frappe.msgprint(_("Reposting entries created: {0}").format(entries))

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
const DIFFERENCE_FIELD_NAMES = [
"difference_in_qty",
"fifo_qty_diff",
"fifo_value_diff",
"fifo_valuation_diff",
"valuation_diff",
"fifo_difference_diff",
"diff_value_diff"
];
frappe.query_reports["Stock Ledger Variance"] = {
"filters": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
}
}
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
get_query: function() {
return {
filters: {is_group: 0, disabled: 0}
}
}
},
{
"fieldname": "difference_in",
"fieldtype": "Select",
"label": "Difference In",
"options": [
"",
"Qty",
"Value",
"Valuation",
],
},
{
"fieldname": "include_disabled",
"fieldtype": "Check",
"label": "Include Disabled",
}
],
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
value = "<span style='color:red'>" + value + "</span>";
}
return value;
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__('Create Reposting Entries'), () => {
let message = `
<div>
<p>
Reposting Entries will change the value of
accounts Stock In Hand, and Stock Expenses
in the Trial Balance report and will also change
the Balance Value in the Stock Balance report.
</p>
<p>Are you sure you want to create Reposting Entries?</p>
</div>`;
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select rows to create Reposting Entries"));
}
frappe.confirm(__(message), () => {
frappe.call({
method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
args: {
rows: selected_rows,
}
});
});
});
},
};

View File

@@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2023-09-20 10:44:19.414449",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-09-20 10:44:19.414449",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Variance",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Stock Ledger Variance",
"report_type": "Script Report",
"roles": []
}

View File

@@ -0,0 +1,279 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import cint, flt
from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import (
get_data as stock_ledger_invariant_check,
)
def execute(filters=None):
columns, data = [], []
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "name",
"fieldtype": "Link",
"label": _("Stock Ledger Entry"),
"options": "Stock Ledger Entry",
},
{
"fieldname": "posting_date",
"fieldtype": "Data",
"label": _("Posting Date"),
},
{
"fieldname": "posting_time",
"fieldtype": "Data",
"label": _("Posting Time"),
},
{
"fieldname": "creation",
"fieldtype": "Data",
"label": _("Creation"),
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": _("Item"),
"options": "Item",
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": _("Warehouse"),
"options": "Warehouse",
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": _("Voucher Type"),
"options": "DocType",
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": _("Voucher No"),
"options": "voucher_type",
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
"label": _("Batchwise Valuation"),
},
{
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": _("Qty Change"),
},
{
"fieldname": "incoming_rate",
"fieldtype": "Float",
"label": _("Incoming Rate"),
},
{
"fieldname": "consumption_rate",
"fieldtype": "Float",
"label": _("Consumption Rate"),
},
{
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"label": _("(A) Qty After Transaction"),
},
{
"fieldname": "expected_qty_after_transaction",
"fieldtype": "Float",
"label": _("(B) Expected Qty After Transaction"),
},
{
"fieldname": "difference_in_qty",
"fieldtype": "Float",
"label": _("A - B"),
},
{
"fieldname": "stock_queue",
"fieldtype": "Data",
"label": _("FIFO/LIFO Queue"),
},
{
"fieldname": "fifo_queue_qty",
"fieldtype": "Float",
"label": _("(C) Total Qty in Queue"),
},
{
"fieldname": "fifo_qty_diff",
"fieldtype": "Float",
"label": _("A - C"),
},
{
"fieldname": "stock_value",
"fieldtype": "Float",
"label": _("(D) Balance Stock Value"),
},
{
"fieldname": "fifo_stock_value",
"fieldtype": "Float",
"label": _("(E) Balance Stock Value in Queue"),
},
{
"fieldname": "fifo_value_diff",
"fieldtype": "Float",
"label": _("D - E"),
},
{
"fieldname": "stock_value_difference",
"fieldtype": "Float",
"label": _("(F) Change in Stock Value"),
},
{
"fieldname": "stock_value_from_diff",
"fieldtype": "Float",
"label": _("(G) Sum of Change in Stock Value"),
},
{
"fieldname": "diff_value_diff",
"fieldtype": "Float",
"label": _("G - D"),
},
{
"fieldname": "fifo_stock_diff",
"fieldtype": "Float",
"label": _("(H) Change in Stock Value (FIFO Queue)"),
},
{
"fieldname": "fifo_difference_diff",
"fieldtype": "Float",
"label": _("H - F"),
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
"label": _("(I) Valuation Rate"),
},
{
"fieldname": "fifo_valuation_rate",
"fieldtype": "Float",
"label": _("(J) Valuation Rate as per FIFO"),
},
{
"fieldname": "fifo_valuation_diff",
"fieldtype": "Float",
"label": _("I - J"),
},
{
"fieldname": "balance_value_by_qty",
"fieldtype": "Float",
"label": _("(K) Valuation = Value (D) ÷ Qty (A)"),
},
{
"fieldname": "valuation_diff",
"fieldtype": "Float",
"label": _("I - K"),
},
]
def get_data(filters=None):
filters = frappe._dict(filters or {})
item_warehouse_map = get_item_warehouse_combinations(filters)
data = []
if item_warehouse_map:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item_warehouse in item_warehouse_map:
report_data = stock_ledger_invariant_check(item_warehouse)
if not report_data:
continue
for row in report_data:
if has_difference(row, precision, filters.difference_in):
data.append(add_item_warehouse_details(row, item_warehouse))
break
return data
def get_item_warehouse_combinations(filters: dict = None) -> dict:
filters = frappe._dict(filters or {})
bin = frappe.qb.DocType("Bin")
item = frappe.qb.DocType("Item")
warehouse = frappe.qb.DocType("Warehouse")
query = (
frappe.qb.from_(bin)
.inner_join(item)
.on(bin.item_code == item.name)
.inner_join(warehouse)
.on(bin.warehouse == warehouse.name)
.select(
bin.item_code,
bin.warehouse,
)
.where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0))
)
if filters.item_code:
query = query.where(item.name == filters.item_code)
if filters.warehouse:
query = query.where(warehouse.name == filters.warehouse)
if not filters.include_disabled:
query = query.where((item.disabled == 0) & (warehouse.disabled == 0))
return query.run(as_dict=1)
def has_difference(row, precision, difference_in):
has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
has_value_difference = (
flt(row.diff_value_diff, precision)
or flt(row.fifo_value_diff, precision)
or flt(row.fifo_difference_diff, precision)
)
has_valuation_difference = flt(row.valuation_diff, precision) or flt(
row.fifo_valuation_diff, precision
)
if difference_in == "Qty" and has_qty_difference:
return True
elif difference_in == "Value" and has_value_difference:
return True
elif difference_in == "Valuation" and has_valuation_difference:
return True
elif difference_in not in ["Qty", "Value", "Valuation"] and (
has_qty_difference or has_value_difference or has_valuation_difference
):
return True
return False
def add_item_warehouse_details(row, item_warehouse):
row.update(
{
"item_code": item_warehouse.item_code,
"warehouse": item_warehouse.warehouse,
}
)
return row