mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-12 11:25:09 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into support-52064
This commit is contained in:
@@ -77,6 +77,7 @@
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
@@ -651,6 +652,12 @@
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -659,7 +666,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified": "2025-10-20 14:06:08.870427",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -75,6 +75,7 @@ class AccountsSettings(Document):
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
use_legacy_controller_for_pcv: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
frappe.ui.form.on("Period Closing Voucher", {
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date());
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ["Process Period Closing Voucher"];
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
|
||||
@@ -132,7 +132,11 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.make_gl_entries()
|
||||
if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
|
||||
self.make_gl_entries()
|
||||
else:
|
||||
ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name})
|
||||
ppcv.save().submit()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -140,11 +144,29 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Account Closing Balance",
|
||||
"Process Period Closing Voucher",
|
||||
)
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
|
||||
if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
|
||||
self.cancel_process_pcv_docs()
|
||||
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.cancel_gl_entries()
|
||||
|
||||
def cancel_process_pcv_docs(self):
|
||||
ppcvs = frappe.db.get_all("Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": 1})
|
||||
for x in ppcvs:
|
||||
frappe.get_doc("Process Period Closing Voucher", x.name).cancel()
|
||||
|
||||
def on_trash(self):
|
||||
super().on_trash()
|
||||
ppcvs = frappe.db.get_all(
|
||||
"Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": ["in", [1, 2]]}
|
||||
)
|
||||
for x in ppcvs:
|
||||
frappe.delete_doc("Process Period Closing Voucher", x.name, force=True, ignore_permissions=True)
|
||||
|
||||
def make_gl_entries(self):
|
||||
if frappe.db.estimate_count("GL Entry") > 100_000:
|
||||
frappe.enqueue(
|
||||
|
||||
@@ -13,6 +13,10 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
def test_closing_entry(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Process Period Closing Voucher", {
|
||||
refresh(frm) {
|
||||
if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) {
|
||||
let execute_btn = __("Start");
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.start_pcv_processing",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
}).then((r) => {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert(__("Job Started"));
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && ["Running"].find((x) => x == frm.doc.status)) {
|
||||
let execute_btn = __("Pause");
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.pause_pcv_processing",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
}).then((r) => {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert(__("PCV Paused"));
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && ["Paused"].find((x) => x == frm.doc.status)) {
|
||||
let execute_btn = __("Resume");
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.resume_pcv_processing",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
}).then((r) => {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert(__("PCV Resumed"));
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// progress bar
|
||||
let progress = 0;
|
||||
|
||||
let normal_finished = frm.doc.normal_balances.filter((x) => x.status == "Completed").length;
|
||||
let opening_finished = frm.doc.z_opening_balances.filter((x) => x.status == "Completed").length;
|
||||
|
||||
progress =
|
||||
((normal_finished + opening_finished) /
|
||||
(frm.doc.normal_balances.length + frm.doc.z_opening_balances.length)) *
|
||||
100;
|
||||
frm.dashboard.add_progress("Books closure progress", progress, "");
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:Process-PCV-{###}",
|
||||
"creation": "2025-09-25 15:44:03.534699",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"parent_pcv",
|
||||
"status",
|
||||
"p_l_closing_balance",
|
||||
"normal_balances",
|
||||
"bs_closing_balance",
|
||||
"z_opening_balances",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "parent_pcv",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "PCV",
|
||||
"options": "Period Closing Voucher",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Queued",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Process Period Closing Voucher",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "p_l_closing_balance",
|
||||
"fieldtype": "JSON",
|
||||
"label": "P&L Closing Balance",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "normal_balances",
|
||||
"fieldtype": "Table",
|
||||
"label": "Dates to Process",
|
||||
"no_copy": 1,
|
||||
"options": "Process Period Closing Voucher Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "z_opening_balances",
|
||||
"fieldtype": "Table",
|
||||
"label": "Opening Balances",
|
||||
"no_copy": 1,
|
||||
"options": "Process Period Closing Voucher Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "bs_closing_balance",
|
||||
"fieldtype": "JSON",
|
||||
"label": "Balance Sheet Closing Balance"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-05 11:40:24.996403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Period Closing Voucher",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Count, Max, Min, Sum
|
||||
from frappe.utils import add_days, flt, get_datetime
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
|
||||
class ProcessPeriodClosingVoucher(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.process_period_closing_voucher_detail.process_period_closing_voucher_detail import (
|
||||
ProcessPeriodClosingVoucherDetail,
|
||||
)
|
||||
|
||||
amended_from: DF.Link | None
|
||||
bs_closing_balance: DF.JSON | None
|
||||
normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
|
||||
p_l_closing_balance: DF.JSON | None
|
||||
parent_pcv: DF.Link
|
||||
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
|
||||
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.status = "Queued"
|
||||
self.populate_processing_tables()
|
||||
|
||||
def populate_processing_tables(self):
|
||||
self.generate_pcv_dates()
|
||||
self.generate_opening_balances_dates()
|
||||
|
||||
def get_dates(self, start, end):
|
||||
return [start + timedelta(days=x) for x in range((end - start).days + 1)]
|
||||
|
||||
def generate_pcv_dates(self):
|
||||
self.normal_balances = []
|
||||
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
|
||||
|
||||
dates = self.get_dates(get_datetime(pcv.period_start_date), get_datetime(pcv.period_end_date))
|
||||
for x in dates:
|
||||
self.append(
|
||||
"normal_balances",
|
||||
{"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"},
|
||||
)
|
||||
self.append(
|
||||
"normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}
|
||||
)
|
||||
|
||||
def generate_opening_balances_dates(self):
|
||||
self.z_opening_balances = []
|
||||
|
||||
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
|
||||
if pcv.is_first_period_closing_voucher():
|
||||
gl = qb.DocType("GL Entry")
|
||||
min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
|
||||
max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
|
||||
|
||||
dates = self.get_dates(get_datetime(min), get_datetime(max))
|
||||
for x in dates:
|
||||
self.append(
|
||||
"z_opening_balances",
|
||||
{"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"},
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
start_pcv_processing(self.name)
|
||||
|
||||
def on_cancel(self):
|
||||
cancel_pcv_processing(self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_pcv_processing(docname: str):
|
||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
if normal_balances := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
fields=["processing_date", "report_type", "parentfield"],
|
||||
order_by="parentfield, idx, processing_date",
|
||||
limit=4,
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
for x in normal_balances:
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{
|
||||
"processing_date": x.processing_date,
|
||||
"parent": docname,
|
||||
"report_type": x.report_type,
|
||||
"parentfield": x.parentfield,
|
||||
},
|
||||
"status",
|
||||
"Running",
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
date=x.processing_date,
|
||||
report_type=x.report_type,
|
||||
parentfield=x.parentfield,
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def pause_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
if queued_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
if queued_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def resume_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
if paused_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Paused"},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
|
||||
start_pcv_processing(docname)
|
||||
|
||||
|
||||
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
|
||||
for i, dimension in enumerate(dimension_fields):
|
||||
gl_entry[dimension] = dimension_values[i]
|
||||
|
||||
|
||||
def get_gle_for_pl_account(pcv, acc, balances, dimensions):
|
||||
balance_in_account_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": pcv.company,
|
||||
"posting_date": pcv.period_end_date,
|
||||
"account": acc,
|
||||
"account_currency": balances.account_currency,
|
||||
"debit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": pcv.name,
|
||||
"fiscal_year": pcv.fiscal_year,
|
||||
"remarks": pcv.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
# update dimensions
|
||||
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
|
||||
def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
|
||||
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
|
||||
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": pcv.company,
|
||||
"posting_date": pcv.period_end_date,
|
||||
"account": pcv.closing_account_head,
|
||||
"account_currency": frappe.db.get_value("Account", pcv.closing_account_head, "account_currency"),
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": pcv.name,
|
||||
"fiscal_year": pcv.fiscal_year,
|
||||
"remarks": pcv.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
# update dimensions
|
||||
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
if to_process := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
fields=["processing_date", "report_type", "parentfield"],
|
||||
order_by="parentfield, idx, processing_date",
|
||||
limit=1,
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{
|
||||
"processing_date": to_process[0].processing_date,
|
||||
"parent": docname,
|
||||
"report_type": to_process[0].report_type,
|
||||
"parentfield": to_process[0].parentfield,
|
||||
},
|
||||
"status",
|
||||
"Running",
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
date=to_process[0].processing_date,
|
||||
report_type=to_process[0].report_type,
|
||||
parentfield=to_process[0].parentfield,
|
||||
)
|
||||
else:
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
total_no_of_dates = (
|
||||
qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0]
|
||||
)
|
||||
completed = (
|
||||
qb.from_(ppcvd)
|
||||
.select(Count(ppcvd.star))
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Completed"))
|
||||
.run()[0][0]
|
||||
)
|
||||
# Ensure both normal and opening balances are processed for all dates
|
||||
if total_no_of_dates == completed:
|
||||
summarize_and_post_ledger_entries(docname)
|
||||
|
||||
|
||||
def make_dict_json_compliant(dimension_wise_balance) -> dict:
|
||||
"""
|
||||
convert tuple -> str
|
||||
JSON doesn't support dictionary with tuple keys
|
||||
"""
|
||||
converted_dict = {}
|
||||
for k, v in dimension_wise_balance.items():
|
||||
str_key = [str(x) for x in k]
|
||||
str_key = ",".join(str_key)
|
||||
converted_dict[str_key] = v
|
||||
|
||||
return converted_dict
|
||||
|
||||
|
||||
def get_consolidated_gles(balances, report_type) -> list:
|
||||
gl_entries = []
|
||||
for x in balances:
|
||||
if x.report_type == report_type:
|
||||
closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)]
|
||||
gl_entries.extend(closing_balances)
|
||||
return gl_entries
|
||||
|
||||
|
||||
def get_gl_entries(docname):
|
||||
"""
|
||||
Calculate total closing balance of all P&L accounts across PCV start and end date
|
||||
"""
|
||||
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
|
||||
|
||||
# calculate balance
|
||||
gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss")
|
||||
pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
|
||||
|
||||
# save
|
||||
json_dict = make_dict_json_compliant(pl_dimension_wise_acc_balance)
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict)
|
||||
)
|
||||
|
||||
# build gl map
|
||||
pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv)
|
||||
pl_accounts_reverse_gle = []
|
||||
closing_account_gle = []
|
||||
|
||||
for dimensions, account_balances in pl_dimension_wise_acc_balance.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if balance_in_company_currency:
|
||||
pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions))
|
||||
|
||||
closing_account_gle.append(get_gle_for_closing_account(pcv, account_balances["balances"], dimensions))
|
||||
|
||||
return pl_accounts_reverse_gle, closing_account_gle
|
||||
|
||||
|
||||
def calculate_balance_sheet_balance(docname):
|
||||
"""
|
||||
Calculate total closing balance of all P&L accounts across PCV start and end date.
|
||||
If it is first PCV, opening entries are also considered
|
||||
"""
|
||||
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
|
||||
gl_entries = get_consolidated_gles(ppcv.normal_balances + ppcv.z_opening_balances, "Balance Sheet")
|
||||
|
||||
# build dimension wise dictionary from all GLE's
|
||||
bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
|
||||
|
||||
# save
|
||||
json_dict = make_dict_json_compliant(bs_dimension_wise_acc_balance)
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict)
|
||||
)
|
||||
return bs_dimension_wise_acc_balance
|
||||
|
||||
|
||||
def get_p_l_closing_entries(pl_gles, pcv):
|
||||
pl_closing_entries = copy.deepcopy(pl_gles)
|
||||
for d in pl_gles:
|
||||
# reverse debit and credit
|
||||
gle_copy = copy.deepcopy(d)
|
||||
gle_copy.debit = d.credit
|
||||
gle_copy.credit = d.debit
|
||||
gle_copy.debit_in_account_currency = d.credit_in_account_currency
|
||||
gle_copy.credit_in_account_currency = d.debit_in_account_currency
|
||||
gle_copy.is_period_closing_voucher_entry = 0
|
||||
gle_copy.period_closing_voucher = pcv.name
|
||||
pl_closing_entries.append(gle_copy)
|
||||
|
||||
return pl_closing_entries
|
||||
|
||||
|
||||
def get_bs_closing_entries(dimension_wise_balance, pcv):
|
||||
closing_entries = []
|
||||
for dimensions, account_balances in dimension_wise_balance.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if acc != "balances" and balance_in_company_currency:
|
||||
closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions))
|
||||
|
||||
return closing_entries
|
||||
|
||||
|
||||
def get_closing_account_closing_entry(closing_account_gle, pcv):
|
||||
closing_entries_for_closing_account = copy.deepcopy(closing_account_gle)
|
||||
for d in closing_entries_for_closing_account:
|
||||
d.period_closing_voucher = pcv.name
|
||||
return closing_entries_for_closing_account
|
||||
|
||||
|
||||
def summarize_and_post_ledger_entries(docname):
|
||||
# P&L accounts
|
||||
pl_accounts_reverse_gle, closing_account_gle = get_gl_entries(docname)
|
||||
gl_entries = pl_accounts_reverse_gle + closing_account_gle
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
|
||||
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
|
||||
pcv = frappe.get_doc("Period Closing Voucher", pcv_name)
|
||||
|
||||
# Balance sheet accounts
|
||||
bs_dimension_wise_acc_balance = calculate_balance_sheet_balance(docname)
|
||||
|
||||
pl_closing_entries = get_p_l_closing_entries(pl_accounts_reverse_gle, pcv)
|
||||
bs_closing_entries = get_bs_closing_entries(bs_dimension_wise_acc_balance, pcv)
|
||||
closing_entries_for_closing_account = get_closing_account_closing_entry(closing_account_gle, pcv)
|
||||
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
|
||||
|
||||
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
|
||||
|
||||
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
|
||||
def get_closing_entry(pcv, account, balances, dimensions):
|
||||
closing_entry = frappe._dict(
|
||||
{
|
||||
"company": pcv.company,
|
||||
"closing_date": pcv.period_end_date,
|
||||
"period_closing_voucher": pcv.name,
|
||||
"account": account,
|
||||
"account_currency": balances.account_currency,
|
||||
"debit_in_account_currency": flt(balances.debit_in_account_currency),
|
||||
"debit": flt(balances.debit),
|
||||
"credit_in_account_currency": flt(balances.credit_in_account_currency),
|
||||
"credit": flt(balances.credit),
|
||||
"is_period_closing_voucher_entry": 0,
|
||||
}
|
||||
)
|
||||
# update dimensions
|
||||
update_default_dimensions(get_dimensions(), closing_entry, dimensions)
|
||||
return closing_entry
|
||||
|
||||
|
||||
def get_dimensions():
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
|
||||
default_dimensions = ["cost_center", "finance_book", "project"]
|
||||
dimensions = default_dimensions + get_accounting_dimensions()
|
||||
return dimensions
|
||||
|
||||
|
||||
def get_dimension_key(res):
|
||||
return tuple([res.get(dimension) for dimension in get_dimensions()])
|
||||
|
||||
|
||||
def build_dimension_wise_balance_dict(gl_entries):
|
||||
dimension_balances = frappe._dict()
|
||||
for x in gl_entries:
|
||||
dimension_key = get_dimension_key(x)
|
||||
dimension_balances.setdefault(dimension_key, frappe._dict()).setdefault(
|
||||
x.account,
|
||||
frappe._dict(
|
||||
{
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"account_currency": x.account_currency,
|
||||
}
|
||||
),
|
||||
)
|
||||
dimension_balances[dimension_key][x.account].debit_in_account_currency += flt(
|
||||
x.debit_in_account_currency
|
||||
)
|
||||
dimension_balances[dimension_key][x.account].credit_in_account_currency += flt(
|
||||
x.credit_in_account_currency
|
||||
)
|
||||
dimension_balances[dimension_key][x.account].debit += flt(x.debit)
|
||||
dimension_balances[dimension_key][x.account].credit += flt(x.credit)
|
||||
|
||||
# dimension-wise total balances
|
||||
dimension_balances[dimension_key].setdefault(
|
||||
"balances",
|
||||
frappe._dict(
|
||||
{
|
||||
"balance_in_account_currency": 0,
|
||||
"balance_in_company_currency": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency)
|
||||
balance_in_company_currency = flt(x.debit) - flt(x.credit)
|
||||
dimension_balances[dimension_key][
|
||||
"balances"
|
||||
].balance_in_account_currency += balance_in_account_currency
|
||||
dimension_balances[dimension_key][
|
||||
"balances"
|
||||
].balance_in_company_currency += balance_in_company_currency
|
||||
|
||||
return dimension_balances
|
||||
|
||||
|
||||
def process_individual_date(docname: str, date, report_type, parentfield):
|
||||
current_date_status = frappe.db.get_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
|
||||
"status",
|
||||
)
|
||||
if current_date_status != "Running":
|
||||
return
|
||||
|
||||
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
|
||||
company = frappe.db.get_value("Period Closing Voucher", pcv_name, "company")
|
||||
|
||||
dimensions = get_dimensions()
|
||||
|
||||
accounts = frappe.db.get_all(
|
||||
"Account", filters={"company": company, "report_type": report_type}, pluck="name"
|
||||
)
|
||||
|
||||
# summarize
|
||||
gle = qb.DocType("GL Entry")
|
||||
query = qb.from_(gle).select(gle.account)
|
||||
for dim in dimensions:
|
||||
query = query.select(gle[dim])
|
||||
query = query.select(
|
||||
Sum(gle.debit).as_("debit"),
|
||||
Sum(gle.credit).as_("credit"),
|
||||
Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
gle.account_currency,
|
||||
).where(
|
||||
(gle.company.eq(company))
|
||||
& (gle.is_cancelled.eq(0))
|
||||
& (gle.posting_date.eq(date))
|
||||
& (gle.account.isin(accounts))
|
||||
)
|
||||
|
||||
if parentfield == "z_opening_balances":
|
||||
query = query.where(gle.is_opening.eq("Yes"))
|
||||
|
||||
query = query.groupby(gle.account)
|
||||
for dim in dimensions:
|
||||
query = query.groupby(gle[dim])
|
||||
res = query.run(as_dict=True)
|
||||
|
||||
# save results
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
|
||||
"closing_balance",
|
||||
frappe.json.dumps(res),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
|
||||
"status",
|
||||
"Completed",
|
||||
)
|
||||
|
||||
# chain call
|
||||
schedule_next_date(docname)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestProcessPeriodClosingVoucher(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for ProcessPeriodClosingVoucher.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-01 15:58:17.544153",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"processing_date",
|
||||
"report_type",
|
||||
"status",
|
||||
"closing_balance"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "processing_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Processing Date"
|
||||
},
|
||||
{
|
||||
"default": "Queued",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_balance",
|
||||
"fieldtype": "JSON",
|
||||
"in_list_view": 1,
|
||||
"label": "Closing Balance"
|
||||
},
|
||||
{
|
||||
"default": "Profit and Loss",
|
||||
"fieldname": "report_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Report Type",
|
||||
"options": "Profit and Loss\nBalance Sheet"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-20 12:03:59.106931",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Period Closing Voucher Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProcessPeriodClosingVoucherDetail(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
closing_balance: DF.JSON | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
processing_date: DF.Date | None
|
||||
report_type: DF.Literal["Profit and Loss", "Balance Sheet"]
|
||||
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -119,6 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"asset_name",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
@@ -143,6 +144,12 @@ def get_columns():
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Name"),
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation Date"),
|
||||
"fieldname": "depreciation_date",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
@@ -10,7 +10,7 @@
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-08-13 12:47:27.645023",
|
||||
"modified": "2025-11-05 15:47:59.597853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
|
||||
@@ -340,7 +340,6 @@
|
||||
"label": "Maintenance Required"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Draft",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@@ -348,7 +347,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -601,7 +600,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-23 00:53:54.249309",
|
||||
"modified": "2025-11-04 22:39:00.817405",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -100,6 +100,7 @@ class Asset(AccountsController):
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
"Submitted",
|
||||
"Cancelled",
|
||||
"Partially Depreciated",
|
||||
"Fully Depreciated",
|
||||
"Sold",
|
||||
@@ -463,6 +464,7 @@ class Asset(AccountsController):
|
||||
"asset_name": self.asset_name,
|
||||
"target_location": self.location,
|
||||
"to_employee": self.custodian,
|
||||
"company": self.company,
|
||||
}
|
||||
]
|
||||
asset_movement = frappe.get_doc(
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -261,7 +261,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-18 15:59:53.981224",
|
||||
"modified": "2025-11-04 23:06:43.644846",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -82,11 +82,21 @@ class AssetRepair(AccountsController):
|
||||
|
||||
def validate_purchase_invoices(self):
|
||||
for d in self.invoices:
|
||||
self.validate_purchase_invoice_status(d.purchase_invoice)
|
||||
invoice_items = self.get_invoice_items(d.purchase_invoice)
|
||||
self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
|
||||
self.validate_expense_account(d, invoice_items)
|
||||
self.validate_purchase_invoice_repair_cost(d, invoice_items)
|
||||
|
||||
def validate_purchase_invoice_status(self, purchase_invoice):
|
||||
docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus")
|
||||
if docstatus == 0:
|
||||
frappe.throw(
|
||||
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
|
||||
get_link_to_form("Purchase Invoice", purchase_invoice)
|
||||
)
|
||||
)
|
||||
|
||||
def get_invoice_items(self, pi):
|
||||
invoice_items = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
|
||||
1223
erpnext/locale/ar.po
1223
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1230
erpnext/locale/bs.po
1230
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/cs.po
1223
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
1229
erpnext/locale/da.po
1229
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
1227
erpnext/locale/de.po
1227
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1227
erpnext/locale/eo.po
1227
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/es.po
1223
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
1225
erpnext/locale/fa.po
1225
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/fr.po
1223
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1228
erpnext/locale/hr.po
1228
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/hu.po
1223
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/id.po
1223
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/it.po
1223
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/my.po
1223
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
1225
erpnext/locale/nb.po
1225
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/nl.po
1223
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/pl.po
1223
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/pt.po
1223
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/ru.po
1223
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
1231
erpnext/locale/sr.po
1231
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1260
erpnext/locale/sv.po
1260
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/ta.po
1223
erpnext/locale/ta.po
File diff suppressed because it is too large
Load Diff
1225
erpnext/locale/th.po
1225
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
1225
erpnext/locale/tr.po
1225
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1223
erpnext/locale/vi.po
1223
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
2156
erpnext/locale/zh.po
2156
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -443,3 +443,4 @@ erpnext.patches.v16_0.update_serial_no_reference_name
|
||||
erpnext.patches.v16_0.rename_subcontracted_quantity
|
||||
erpnext.patches.v16_0.add_new_stock_entry_types
|
||||
erpnext.patches.v15_0.set_asset_status_if_not_already_set
|
||||
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Description:
|
||||
Enable Legacy controller for Period Closing Voucher
|
||||
"""
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
@@ -1026,7 +1026,14 @@ class TestDeliveryNote(IntegrationTestCase):
|
||||
def test_sales_invoice_qty_after_return(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
dn = create_delivery_note(qty=10)
|
||||
item = make_item(
|
||||
"Test Sales Invoice Qty After Return",
|
||||
properties={"is_stock_item": 1, "stock_uom": "Nos"},
|
||||
)
|
||||
|
||||
make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||
|
||||
dn = create_delivery_note(item_code=item.name, qty=10)
|
||||
|
||||
dnr1 = make_sales_return(dn.name)
|
||||
dnr1.get("items")[0].qty = -3
|
||||
@@ -1042,8 +1049,8 @@ class TestDeliveryNote(IntegrationTestCase):
|
||||
self.assertEqual(si.get("items")[0].qty, 5)
|
||||
|
||||
si.reload().cancel().delete()
|
||||
dnr1.reload().cancel().delete()
|
||||
dnr2.reload().cancel().delete()
|
||||
dnr1.reload().cancel().delete()
|
||||
dn.reload().cancel().delete()
|
||||
|
||||
def test_dn_billing_status_case3(self):
|
||||
|
||||
@@ -87,7 +87,9 @@ frappe.ui.form.on("Material Request", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
if (!frm.doc.buying_price_list) {
|
||||
frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
}
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
|
||||
@@ -311,6 +311,30 @@ class SerialandBatchBundle(Document):
|
||||
SerialNoDuplicateError,
|
||||
)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Stock Entry"
|
||||
and self.type_of_transaction == "Inward"
|
||||
and frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose")
|
||||
in ["Manufacture", "Repack"]
|
||||
):
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial No", filters={"name": ("in", serial_nos), "status": "Delivered"}, pluck="name"
|
||||
)
|
||||
|
||||
if serial_nos:
|
||||
if len(serial_nos) == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry."
|
||||
).format(bold(serial_nos[0]))
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry."
|
||||
).format(bold(", ".join(serial_nos)))
|
||||
)
|
||||
|
||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||
frappe.throw(_(message), exception, title=_("Error"))
|
||||
|
||||
|
||||
@@ -2147,6 +2147,45 @@ class TestStockEntry(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(incoming_rate, 125.0)
|
||||
|
||||
def test_prevent_reuse_delivered_serial_no_in_repack(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
item = "Test Prevent Reuse Delivered Serial No"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"})
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100)
|
||||
make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100)
|
||||
|
||||
dn = create_delivery_note(item_code=item, qty=2)
|
||||
delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True
|
||||
)
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_doc.name,
|
||||
"item_name": item_doc.item_name,
|
||||
"s_warehouse": None,
|
||||
"t_warehouse": warehouse,
|
||||
"description": item_doc.description,
|
||||
"uom": item_doc.stock_uom,
|
||||
"qty": 1,
|
||||
"use_serial_batch_fields": 1,
|
||||
"serial_no": delivered_serial_no,
|
||||
},
|
||||
)
|
||||
|
||||
se.save()
|
||||
status = frappe.db.get_value("Serial No", delivered_serial_no, "status")
|
||||
|
||||
self.assertEqual(status, "Delivered")
|
||||
self.assertEqual(se.purpose, "Repack")
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def make_serialized_item(self, **args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -419,12 +419,7 @@ class SerialBatchBundle:
|
||||
|
||||
self.update_serial_no_status_warehouse(self.sle, serial_nos)
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
def get_status_for_serial_nos(self, sle):
|
||||
status = "Inactive"
|
||||
if sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
@@ -438,6 +433,23 @@ class SerialBatchBundle:
|
||||
]:
|
||||
status = "Consumed"
|
||||
|
||||
if sle.is_cancelled == 1 and (
|
||||
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
|
||||
):
|
||||
status = "Inactive"
|
||||
|
||||
return status
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
status = "Active"
|
||||
if not warehouse:
|
||||
status = self.get_status_for_serial_nos(sle)
|
||||
|
||||
customer = None
|
||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, bold, scrub
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
@@ -67,8 +68,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
|
||||
if sl_entries:
|
||||
cancel = sl_entries[0].get("is_cancelled")
|
||||
if cancel:
|
||||
cancelled = sl_entries[0].get("is_cancelled")
|
||||
if cancelled:
|
||||
validate_cancellation(sl_entries)
|
||||
set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
|
||||
|
||||
@@ -79,7 +80,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
if sle.serial_no and not via_landed_cost_voucher:
|
||||
validate_serial_no(sle)
|
||||
|
||||
if cancel:
|
||||
if cancelled:
|
||||
sle["actual_qty"] = -flt(sle.get("actual_qty"))
|
||||
|
||||
if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
|
||||
@@ -108,7 +109,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
if is_stock_item:
|
||||
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
|
||||
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
repost_current_voucher(
|
||||
args, allow_negative_stock, via_landed_cost_voucher, cancelled=cancelled
|
||||
)
|
||||
update_bin_qty(bin_name, args)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
@@ -116,7 +119,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
)
|
||||
|
||||
|
||||
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False, cancelled=False):
|
||||
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
|
||||
if not args.get("posting_date"):
|
||||
args["posting_date"] = nowdate()
|
||||
@@ -135,6 +138,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
|
||||
"sle_id": args.get("name"),
|
||||
"creation": args.get("creation"),
|
||||
"reserved_stock": args.get("reserved_stock"),
|
||||
"cancelled": cancelled,
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
@@ -667,33 +671,31 @@ class update_entries_after:
|
||||
def process_sle_against_current_timestamp(self):
|
||||
sl_entries = self.get_sle_against_current_voucher()
|
||||
for sle in sl_entries:
|
||||
sle["timestamp"] = sle.posting_datetime
|
||||
self.process_sle(sle)
|
||||
|
||||
def get_sle_against_current_voucher(self):
|
||||
self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time)
|
||||
doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
*, posting_datetime as "timestamp"
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
and (
|
||||
posting_datetime = %(posting_datetime)s
|
||||
)
|
||||
and creation = %(creation)s
|
||||
order by
|
||||
creation ASC
|
||||
for update
|
||||
""",
|
||||
self.args,
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select("*")
|
||||
.where(
|
||||
(doctype.item_code == self.args.item_code)
|
||||
& (doctype.warehouse == self.args.warehouse)
|
||||
& (doctype.is_cancelled == 0)
|
||||
& (doctype.posting_datetime == self.args.posting_datetime)
|
||||
)
|
||||
.orderby(doctype.creation, order=Order.asc)
|
||||
.for_update()
|
||||
)
|
||||
|
||||
if not self.args.get("cancelled"):
|
||||
query = query.where(doctype.creation == self.args.creation)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def get_future_entries_to_fix(self):
|
||||
# includes current entry!
|
||||
args = self.data[self.args.warehouse].previous_sle or frappe._dict(
|
||||
@@ -1715,7 +1717,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
voucher_no = args.get("voucher_no")
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
elif args.get("creation") and args.get("sle_id"):
|
||||
elif args.get("creation") and args.get("sle_id") and not args.get("cancelled"):
|
||||
creation = args.get("creation")
|
||||
operator = "<="
|
||||
voucher_condition = f"and creation < '{creation}'"
|
||||
|
||||
Reference in New Issue
Block a user