mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-23 11:09:41 +00:00
Compare commits
334 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ccdb987d9 | ||
|
|
a6c58a3542 | ||
|
|
ceccd8c2dc | ||
|
|
a38819cbd5 | ||
|
|
801940912a | ||
|
|
b665e4e24a | ||
|
|
e4d94af019 | ||
|
|
eaf6d0d7d8 | ||
|
|
1b8bd0e1f3 | ||
|
|
ce817cbc12 | ||
|
|
84a40c282b | ||
|
|
9f7afda4db | ||
|
|
ddd50167a5 | ||
|
|
7ad664d89a | ||
|
|
5eb252215c | ||
|
|
10d8cc9d66 | ||
|
|
64f616b8a7 | ||
|
|
9a2b0a965d | ||
|
|
ec465571d8 | ||
|
|
ce42d847b3 | ||
|
|
a689830bff | ||
|
|
9bfcad31fd | ||
|
|
426010e21a | ||
|
|
ba09ddcc3a | ||
|
|
d0a655d5ae | ||
|
|
91a276d4ed | ||
|
|
16c1fc75b5 | ||
|
|
7f9ae4e044 | ||
|
|
c8be4f3f78 | ||
|
|
d830ce1d88 | ||
|
|
07a394a1c5 | ||
|
|
68a95c7dbc | ||
|
|
164d7cc896 | ||
|
|
063cef576c | ||
|
|
c6bfdcf503 | ||
|
|
085e0455d8 | ||
|
|
cb36dcb382 | ||
|
|
ffd426d43d | ||
|
|
d84a3c4f29 | ||
|
|
a12df122a9 | ||
|
|
df25d33735 | ||
|
|
a6c26874c7 | ||
|
|
54f758c327 | ||
|
|
bf0b74bcbd | ||
|
|
0d02f8b5d1 | ||
|
|
b343334f69 | ||
|
|
4a749cec72 | ||
|
|
3648f3816f | ||
|
|
6da359a839 | ||
|
|
5ea498062c | ||
|
|
4dcaf42bc5 | ||
|
|
db3be4195c | ||
|
|
9a526611e0 | ||
|
|
9ac87bd3b1 | ||
|
|
0171af4604 | ||
|
|
3ab31dcb92 | ||
|
|
63ecf13058 | ||
|
|
8218ca990c | ||
|
|
ac121dd4e3 | ||
|
|
870709079b | ||
|
|
302f049025 | ||
|
|
63668eb855 | ||
|
|
9a93c892be | ||
|
|
1af2326a52 | ||
|
|
ab162070a6 | ||
|
|
d8e1a21bdc | ||
|
|
c2eb771c4d | ||
|
|
8a72845ee6 | ||
|
|
7189daba19 | ||
|
|
93d0db2910 | ||
|
|
50db0aca61 | ||
|
|
b5d2708f69 | ||
|
|
becfd980b2 | ||
|
|
be6cd6adc3 | ||
|
|
48939f25c8 | ||
|
|
94a03c6e17 | ||
|
|
cd4746ad2a | ||
|
|
e22d0a3406 | ||
|
|
1755006445 | ||
|
|
4c9e17fbd3 | ||
|
|
d06831ea94 | ||
|
|
603d2cf77d | ||
|
|
13557a2c10 | ||
|
|
218c51cdcf | ||
|
|
61a11c8f1f | ||
|
|
b21abf4d90 | ||
|
|
c70282663c | ||
|
|
87e0d2f7f4 | ||
|
|
8bfc212e26 | ||
|
|
a937e32989 | ||
|
|
935f2e11e8 | ||
|
|
7202939e0d | ||
|
|
73a8b6a7d7 | ||
|
|
303ae8321b | ||
|
|
d5e1a46588 | ||
|
|
27108874ea | ||
|
|
29aa5d6468 | ||
|
|
a826a894f4 | ||
|
|
9a5604c5bb | ||
|
|
b95dfcbce0 | ||
|
|
ed01b4c161 | ||
|
|
19db526fdd | ||
|
|
11dd1968c7 | ||
|
|
07aaef2af2 | ||
|
|
20478b632f | ||
|
|
18f32b8de6 | ||
|
|
81297ce168 | ||
|
|
f48ce90658 | ||
|
|
3df68e462f | ||
|
|
a79bc4d35a | ||
|
|
bf4fb53575 | ||
|
|
cf25f4c579 | ||
|
|
188645bfd6 | ||
|
|
1c4eef2ef6 | ||
|
|
a3bf320add | ||
|
|
d0db3b08d7 | ||
|
|
2fb441763a | ||
|
|
33fa1e45ad | ||
|
|
d392660d45 | ||
|
|
ced76ca5c0 | ||
|
|
9c0f17e13d | ||
|
|
e0a45a5a54 | ||
|
|
692de892ae | ||
|
|
879b2b778a | ||
|
|
12530616a7 | ||
|
|
41db040a60 | ||
|
|
40927c7413 | ||
|
|
58a3ef7aa6 | ||
|
|
9ddf1ccedd | ||
|
|
0495160f81 | ||
|
|
d2e5b2aa1d | ||
|
|
39387e9f54 | ||
|
|
2205ae8e54 | ||
|
|
29fe23bc0a | ||
|
|
8d97966662 | ||
|
|
c99d0535f8 | ||
|
|
fac27d9dff | ||
|
|
0519263882 | ||
|
|
71479ad47b | ||
|
|
88f5e3f160 | ||
|
|
71837ab400 | ||
|
|
853ca1fcee | ||
|
|
50dd8d9df7 | ||
|
|
aeaadb1e30 | ||
|
|
2e0cf36901 | ||
|
|
1d5345abc1 | ||
|
|
0a03076148 | ||
|
|
a3b8f9759d | ||
|
|
f51c511bcc | ||
|
|
9e56f213a3 | ||
|
|
1fa9030aee | ||
|
|
62226696aa | ||
|
|
605a30a7e7 | ||
|
|
0996aff79d | ||
|
|
501c53db05 | ||
|
|
f343d5a24d | ||
|
|
04fbcc64ff | ||
|
|
148d7e798b | ||
|
|
f7717c91bc | ||
|
|
c05382fa48 | ||
|
|
b673377b70 | ||
|
|
6bbc8e0544 | ||
|
|
ddb38db5c4 | ||
|
|
03b5d5a0e0 | ||
|
|
f70506fc92 | ||
|
|
a58ce52729 | ||
|
|
a5d9f5518f | ||
|
|
1281d9d21d | ||
|
|
52be45c5df | ||
|
|
08cabd1717 | ||
|
|
60ffcd0574 | ||
|
|
6f98fe15e4 | ||
|
|
601ea444ca | ||
|
|
ab20344141 | ||
|
|
752d175d22 | ||
|
|
a33d5535a7 | ||
|
|
99ead94ffe | ||
|
|
e05ae14d49 | ||
|
|
cd9f949b12 | ||
|
|
aef544cd53 | ||
|
|
d34025dc11 | ||
|
|
4db12fe2da | ||
|
|
ea12897ce9 | ||
|
|
802d9b2d4a | ||
|
|
492ba539e8 | ||
|
|
21a7dd43a9 | ||
|
|
50d1fa4665 | ||
|
|
e10a58074f | ||
|
|
b2deb89826 | ||
|
|
c490a66540 | ||
|
|
afa0c13587 | ||
|
|
4dbee00b82 | ||
|
|
5d6fc71556 | ||
|
|
06dd5e0071 | ||
|
|
105f9ec2e1 | ||
|
|
dc6fdbb836 | ||
|
|
8668ae92d8 | ||
|
|
ec7e5c48de | ||
|
|
85088e4aff | ||
|
|
c2c6d27625 | ||
|
|
a70181e025 | ||
|
|
86017b223a | ||
|
|
930e79c351 | ||
|
|
2c7f5ec324 | ||
|
|
a9f5e86600 | ||
|
|
d6decf9172 | ||
|
|
1fac17b36f | ||
|
|
9d05a6ebc0 | ||
|
|
389ee909a5 | ||
|
|
e5aaa5b6e5 | ||
|
|
f3ceb4ac7d | ||
|
|
050ca4b726 | ||
|
|
928b6b1510 | ||
|
|
ef1e121bd4 | ||
|
|
68f1b41969 | ||
|
|
a329003f7f | ||
|
|
cf1eabe049 | ||
|
|
4c78a682ad | ||
|
|
4752ed2483 | ||
|
|
e56dd8268b | ||
|
|
e0477cf59f | ||
|
|
8c115e146b | ||
|
|
6267ab994c | ||
|
|
8bf8bcf739 | ||
|
|
eed02d3f44 | ||
|
|
6265582e53 | ||
|
|
361836e735 | ||
|
|
ae73d9c621 | ||
|
|
2c2ca22d12 | ||
|
|
e37a88fdb6 | ||
|
|
9c26093a51 | ||
|
|
5ce2d73692 | ||
|
|
ca0a962870 | ||
|
|
b72906a7a1 | ||
|
|
d604b12d51 | ||
|
|
d46cf46375 | ||
|
|
c3f6edcd01 | ||
|
|
120b481c4a | ||
|
|
029021f035 | ||
|
|
9e109acec7 | ||
|
|
355ba2f632 | ||
|
|
83ce3dd915 | ||
|
|
60508a9706 | ||
|
|
660d20f7fa | ||
|
|
0b2603bbf1 | ||
|
|
5660e8b26d | ||
|
|
cf0fa0db7b | ||
|
|
fc9a3c0c92 | ||
|
|
2b4cd0a9bb | ||
|
|
6decb7cc34 | ||
|
|
9039b86e8a | ||
|
|
ee8485a54a | ||
|
|
05db28c64f | ||
|
|
bcd0105915 | ||
|
|
225adf5cbc | ||
|
|
af5947edd0 | ||
|
|
d6f70f533a | ||
|
|
1dd4168c0e | ||
|
|
db4360d76c | ||
|
|
8ce81a058a | ||
|
|
55464c79c4 | ||
|
|
0c599c2b6d | ||
|
|
eb1f1255eb | ||
|
|
62cc86114b | ||
|
|
5268da2e55 | ||
|
|
d695fea251 | ||
|
|
6b2983d8c1 | ||
|
|
85d74050e1 | ||
|
|
d69a974a4d | ||
|
|
3d9d56ab50 | ||
|
|
dbd7b83204 | ||
|
|
6770610c6d | ||
|
|
75916629c8 | ||
|
|
e785928c0f | ||
|
|
edfa6e41e1 | ||
|
|
5a9522e70f | ||
|
|
4fa5131590 | ||
|
|
1b28a4e928 | ||
|
|
f2a72e5f82 | ||
|
|
661eb058b9 | ||
|
|
6516e68fa0 | ||
|
|
35a08f8830 | ||
|
|
562327f041 | ||
|
|
8e7d893669 | ||
|
|
d96cee8779 | ||
|
|
96c4d1af63 | ||
|
|
9c0a17e4d5 | ||
|
|
ee2c8c869a | ||
|
|
15b34a607f | ||
|
|
4fc6d3ef64 | ||
|
|
7bf6251c21 | ||
|
|
5fc5934942 | ||
|
|
01f9139ebd | ||
|
|
4be557bdce | ||
|
|
96d8b5242d | ||
|
|
67bd540135 | ||
|
|
e730b8c6e4 | ||
|
|
30fd11f138 | ||
|
|
0986d3ebe4 | ||
|
|
c551c2714c | ||
|
|
efc97cc59f | ||
|
|
21a01575b6 | ||
|
|
6f3b5604b9 | ||
|
|
50e47e796d | ||
|
|
4d3e43bdbe | ||
|
|
928c887de5 | ||
|
|
2984bad2c0 | ||
|
|
2d09ef2509 | ||
|
|
8b5997e38f | ||
|
|
86b10ce9bb | ||
|
|
2c4610c021 | ||
|
|
4b3f143f83 | ||
|
|
d495d93840 | ||
|
|
a1b6628c41 | ||
|
|
770bc1c293 | ||
|
|
907e3af1b0 | ||
|
|
e706aa692a | ||
|
|
a5fa287dad | ||
|
|
c1f14f2991 | ||
|
|
6d66002374 | ||
|
|
27cd51e267 | ||
|
|
1d6f97ad94 | ||
|
|
ea69ba7cd8 | ||
|
|
74c880c232 | ||
|
|
47f06dc180 | ||
|
|
0a70b3ffcc | ||
|
|
4278b08147 | ||
|
|
f36a68b42b | ||
|
|
5a2a404a50 | ||
|
|
ef10c4ea4f | ||
|
|
9f970189fe | ||
|
|
8db11d03ed | ||
|
|
2c21df2ad9 | ||
|
|
b80a5f27a9 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.36.2"
|
||||
__version__ = "15.41.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += f"AND {deferred_account}='{account}'"
|
||||
conditions += f"AND {deferred_account}='{frappe.db.escape(account)}'"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
|
||||
@@ -113,9 +113,9 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = []
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
|
||||
filters={"docstatus": 1, "company": company, "period_end_date": ("<", closing_date)},
|
||||
fields=["name"],
|
||||
order_by="posting_date desc",
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ frappe.ui.form.on("Accounting Dimension", {
|
||||
},
|
||||
|
||||
label: function (frm) {
|
||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
|
||||
frm.set_value("fieldname", frm.doc.label.replace(/ /g, "_").replace(/-/g, "_").toLowerCase());
|
||||
},
|
||||
|
||||
document_type: function (frm) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.database.schema import validate_column_name
|
||||
from frappe.model import core_doctypes_list
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr
|
||||
@@ -60,6 +61,7 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
|
||||
@@ -101,6 +101,8 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
date = doc.available_for_use_date
|
||||
elif doc.doctype == "Asset Repair":
|
||||
date = doc.completion_date
|
||||
elif doc.doctype == "Period Closing Voucher":
|
||||
date = doc.period_end_date
|
||||
else:
|
||||
date = doc.posting_date
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-10-16 16:57:12.085072",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Against Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Against Voucher No",
|
||||
"options": "against_voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "event",
|
||||
"fieldtype": "Data",
|
||||
"label": "Event",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(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
|
||||
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase):
|
||||
"""
|
||||
Integration tests for AdvancePaymentLedgerEntry.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return so
|
||||
|
||||
def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
po = create_purchase_order(
|
||||
company=self.company,
|
||||
customer=self.supplier,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return po
|
||||
|
||||
def test_so_advance_paid_and_currency_with_payment(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from = self.debtors_usd
|
||||
pe.paid_from_account_currency = "USD"
|
||||
pe.source_exchange_rate = pe_exchange_rate
|
||||
pe.paid_amount = so.grand_total
|
||||
pe.received_amount = pe_exchange_rate * pe.paid_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_so_advance_paid_and_currency_with_journal(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": so.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debtors_usd,
|
||||
"party_type": "Customer",
|
||||
"party": so.customer,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": so.doctype,
|
||||
"reference_name": so.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_payment(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.target_exchange_rate = pe_exchange_rate
|
||||
pe.received_amount = po.grand_total
|
||||
pe.paid_amount = pe_exchange_rate * pe.received_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_journal(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": po.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.creditors_usd,
|
||||
"party_type": "Supplier",
|
||||
"party": po.supplier,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": po.doctype,
|
||||
"reference_name": po.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
@@ -224,11 +224,6 @@
|
||||
"link_doctype": "Bank Guarantee",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payroll Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Transaction",
|
||||
@@ -255,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-09-24 06:57:41.292970",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -168,7 +168,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "frankfurter.app/{transaction_date}"
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
|
||||
DOCTYPE = "Dunning Letter Text"
|
||||
FIELDS = ["body_text", "closing_text", "language"]
|
||||
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if not language:
|
||||
language = doc.get("language")
|
||||
|
||||
if language:
|
||||
filters = {"parent": dunning_type, "language": language}
|
||||
else:
|
||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
||||
letter_text = frappe.db.get_value(
|
||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
||||
)
|
||||
if letter_text:
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
frappe.ui.form.on("Fiscal Year", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value(
|
||||
"year_start_date",
|
||||
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)
|
||||
);
|
||||
frm.set_value("year_start_date", frappe.datetime.year_start());
|
||||
}
|
||||
},
|
||||
year_start_date: function (frm) {
|
||||
|
||||
@@ -6,38 +6,50 @@
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dates_section",
|
||||
"posting_date",
|
||||
"transaction_date",
|
||||
"column_break_avko",
|
||||
"fiscal_year",
|
||||
"due_date",
|
||||
"account_details_section",
|
||||
"account",
|
||||
"account_currency",
|
||||
"column_break_ifvf",
|
||||
"against",
|
||||
"party_type",
|
||||
"party",
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"against",
|
||||
"transaction_details_section",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"voucher_subtype",
|
||||
"transaction_currency",
|
||||
"column_break_dpsx",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
"voucher_type",
|
||||
"voucher_subtype",
|
||||
"voucher_no",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
"project",
|
||||
"remarks",
|
||||
"more_info_section",
|
||||
"finance_book",
|
||||
"company",
|
||||
"is_opening",
|
||||
"is_advance",
|
||||
"fiscal_year",
|
||||
"company",
|
||||
"finance_book",
|
||||
"column_break_8abq",
|
||||
"to_rename",
|
||||
"due_date",
|
||||
"is_cancelled",
|
||||
"transaction_currency",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_exchange_rate"
|
||||
"remarks"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -285,13 +297,67 @@
|
||||
"fieldname": "voucher_subtype",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Voucher Subtype"
|
||||
},
|
||||
{
|
||||
"fieldname": "dates_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dates"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_avko",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Account Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ifvf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "amounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dpsx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bm1w",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lmnm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-02 14:31:51.496466",
|
||||
"modified": "2024-08-22 13:03:39.997475",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
|
||||
@@ -430,8 +430,9 @@ def update_against_account(voucher_type, voucher_no):
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
|
||||
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
|
||||
frappe.db.add_index("GL Entry", ["posting_date", "company"])
|
||||
frappe.db.add_index("GL Entry", ["party_type", "party"])
|
||||
|
||||
|
||||
def rename_gle_sle_docs():
|
||||
|
||||
@@ -188,6 +188,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
@@ -218,8 +219,10 @@ class JournalEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
@@ -259,7 +262,7 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, self.posting_date, self.company
|
||||
@@ -1668,6 +1671,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
"reference_type": "reference_type",
|
||||
"reference_name": "reference_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -174,6 +174,17 @@ frappe.ui.form.on("Payment Entry", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
return {
|
||||
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -191,7 +202,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.add_fetch(
|
||||
"payment_request",
|
||||
"outstanding_amount",
|
||||
"payment_request_outstanding",
|
||||
"Payment Entry Reference"
|
||||
);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
@@ -216,6 +235,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
}
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -797,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
@@ -818,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type == "Receive")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
@@ -989,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
c.outstanding_amount = d.outstanding_amount;
|
||||
c.bill_no = d.bill_no;
|
||||
c.payment_term = d.payment_term;
|
||||
c.payment_term_outstanding = d.payment_term_outstanding;
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
@@ -1038,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
frm.events.allocate_party_amount_against_ref_docs(
|
||||
frm,
|
||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
|
||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
|
||||
false
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1052,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return ["Sales Invoice", "Purchase Invoice"];
|
||||
},
|
||||
|
||||
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
|
||||
var total_positive_outstanding_including_order = 0;
|
||||
var total_negative_outstanding = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
|
||||
paid_amount -= total_deductions;
|
||||
|
||||
$.each(frm.doc.references || [], function (i, row) {
|
||||
if (flt(row.outstanding_amount) > 0)
|
||||
total_positive_outstanding_including_order += flt(row.outstanding_amount);
|
||||
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
|
||||
var allocated_negative_outstanding = 0;
|
||||
if (
|
||||
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
|
||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
|
||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
|
||||
) {
|
||||
if (total_positive_outstanding_including_order > paid_amount) {
|
||||
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
|
||||
allocated_negative_outstanding =
|
||||
total_negative_outstanding < remaining_outstanding
|
||||
? total_negative_outstanding
|
||||
: remaining_outstanding;
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
|
||||
if (paid_amount > total_negative_outstanding) {
|
||||
if (total_negative_outstanding == 0) {
|
||||
frappe.msgprint(
|
||||
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
|
||||
frm.doc.payment_type,
|
||||
frm.doc.party_type == "Customer" ? "to" : "from",
|
||||
frm.doc.party_type,
|
||||
])
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
|
||||
total_negative_outstanding,
|
||||
])
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
|
||||
allocated_negative_outstanding =
|
||||
paid_amount +
|
||||
(total_positive_outstanding_including_order < allocated_positive_outstanding
|
||||
? total_positive_outstanding_including_order
|
||||
: allocated_positive_outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
$.each(frm.doc.references || [], function (i, row) {
|
||||
if (frappe.flags.allocate_payment_amount == 0) {
|
||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
||||
row.allocated_amount = 0;
|
||||
} else if (
|
||||
frappe.flags.allocate_payment_amount != 0 &&
|
||||
(!row.allocated_amount || paid_amount_change)
|
||||
) {
|
||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
||||
row.allocated_amount =
|
||||
row.outstanding_amount >= allocated_positive_outstanding
|
||||
? allocated_positive_outstanding
|
||||
: row.outstanding_amount;
|
||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
||||
row.allocated_amount =
|
||||
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
|
||||
? -1 * allocated_negative_outstanding
|
||||
: row.outstanding_amount;
|
||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.refresh_fields();
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
},
|
||||
|
||||
@@ -1686,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
return current_tax_amount;
|
||||
},
|
||||
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
const { matched_payment_requests } = frappe.last_response;
|
||||
if (!matched_payment_requests) return;
|
||||
|
||||
const COLUMN_LABEL = [
|
||||
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
|
||||
];
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Unset Matched Payment Request"),
|
||||
message: COLUMN_LABEL.concat(matched_payment_requests),
|
||||
as_table: true,
|
||||
wide: true,
|
||||
primary_action: {
|
||||
label: __("Allocate Payment Request"),
|
||||
action() {
|
||||
frappe.hide_msgprint();
|
||||
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
|
||||
frm.dirty();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Reference", {
|
||||
@@ -1778,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
frappe.ui.form.on("Payment Entry", {
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||
from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
@@ -98,13 +100,18 @@ class PaymentEntry(AccountsController):
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
def before_save(self):
|
||||
self.set_matched_unset_payment_requests_to_response()
|
||||
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def set_liability_account(self):
|
||||
@@ -184,34 +191,40 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.set_payment_req_status()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def set_payment_req_status(self):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
||||
def update_payment_requests(self, cancel=False):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
update_payment_req_status(self, None)
|
||||
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
|
||||
|
||||
def update_outstanding_amounts(self):
|
||||
self.set_missing_ref_details(force=True)
|
||||
|
||||
def validate_duplicate_entry(self):
|
||||
reference_names = []
|
||||
reference_names = set()
|
||||
for d in self.get("references"):
|
||||
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
|
||||
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
|
||||
if key in reference_names:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
||||
d.idx, d.reference_doctype, d.reference_name
|
||||
)
|
||||
)
|
||||
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
|
||||
|
||||
reference_names.add(key)
|
||||
|
||||
def set_bank_account_data(self):
|
||||
if self.bank_account:
|
||||
@@ -237,6 +250,8 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.validate_allocated_amount_as_per_payment_request()
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
@@ -249,6 +264,27 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_as_per_payment_request(self):
|
||||
"""
|
||||
Allocated amount should not be greater than the outstanding amount of the Payment Request.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
|
||||
|
||||
if not pr_outstanding_amounts:
|
||||
return
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
|
||||
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
def term_based_allocation_enabled_for_reference(
|
||||
self, reference_doctype: str, reference_name: str
|
||||
) -> bool:
|
||||
@@ -1133,6 +1169,8 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1178,11 +1216,30 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
@@ -1606,6 +1663,380 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return current_tax_fraction
|
||||
|
||||
def set_matched_unset_payment_requests_to_response(self):
|
||||
"""
|
||||
Find matched Payment Requests for those references which have no Payment Request set.\n
|
||||
And set to `frappe.response` to show in the frontend for allocation.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
matched_payment_requests = get_matched_payment_request_of_references(
|
||||
[row for row in self.references if not row.payment_request]
|
||||
)
|
||||
|
||||
if not matched_payment_requests:
|
||||
return
|
||||
|
||||
frappe.response["matched_payment_requests"] = matched_payment_requests
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
|
||||
"""
|
||||
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
|
||||
:param paid_amount: Paid Amount / Received Amount.
|
||||
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
|
||||
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
if not allocate_payment_amount:
|
||||
for ref in self.references:
|
||||
ref.allocated_amount = 0
|
||||
return
|
||||
|
||||
# calculating outstanding amounts
|
||||
precision = self.precision("paid_amount")
|
||||
total_positive_outstanding_including_order = 0
|
||||
total_negative_outstanding = 0
|
||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||
|
||||
for ref in self.references:
|
||||
reference_outstanding_amount = ref.outstanding_amount
|
||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||
|
||||
if reference_outstanding_amount > 0:
|
||||
total_positive_outstanding_including_order += abs_outstanding_amount
|
||||
else:
|
||||
total_negative_outstanding += abs_outstanding_amount
|
||||
|
||||
# calculating allocated outstanding amounts
|
||||
allocated_negative_outstanding = 0
|
||||
allocated_positive_outstanding = 0
|
||||
|
||||
# checking party type and payment type
|
||||
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
|
||||
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
|
||||
):
|
||||
if total_positive_outstanding_including_order > paid_amount:
|
||||
remaining_outstanding = flt(
|
||||
total_positive_outstanding_including_order - paid_amount, precision
|
||||
)
|
||||
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
|
||||
allocated_negative_outstanding = paid_amount + min(
|
||||
total_positive_outstanding_including_order, allocated_positive_outstanding
|
||||
)
|
||||
|
||||
# inner function to set `allocated_amount` to those row which have no PR
|
||||
def _allocation_to_unset_pr_row(
|
||||
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
|
||||
):
|
||||
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - row.allocated_amount, precision
|
||||
)
|
||||
elif outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - abs(row.allocated_amount), precision
|
||||
)
|
||||
return allocated_positive_outstanding, allocated_negative_outstanding
|
||||
|
||||
# allocate amount based on `paid_amount` is changed or not
|
||||
if not paid_amount_change:
|
||||
for ref in self.references:
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
ref.outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
|
||||
|
||||
else:
|
||||
payment_request_outstanding_amounts = (
|
||||
get_payment_request_outstanding_set_in_references(self.references) or {}
|
||||
)
|
||||
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
|
||||
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = references_outstanding_amounts[key]
|
||||
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
|
||||
|
||||
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_positive_outstanding,
|
||||
reference_outstanding_amount,
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts)
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = ref.allocated_amount
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] = flt(
|
||||
remaining_references_allocated_amounts[key] - allocated_amount, precision
|
||||
)
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
|
||||
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_negative_outstanding,
|
||||
abs(reference_outstanding_amount),
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts) * -1
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = abs(ref.allocated_amount)
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = remaining_references_allocated_amounts[key]
|
||||
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
reference_outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_matched_payment_requests(self, matched_payment_requests):
|
||||
"""
|
||||
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
|
||||
:param matched_payment_requests: List of tuple of matched Payment Requests.
|
||||
|
||||
---
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not self.references or not matched_payment_requests:
|
||||
return
|
||||
|
||||
if isinstance(matched_payment_requests, str):
|
||||
matched_payment_requests = json.loads(matched_payment_requests)
|
||||
|
||||
# modify matched_payment_requests
|
||||
# like (reference_doctype, reference_name, allocated_amount): payment_request
|
||||
payment_requests = {}
|
||||
|
||||
for row in matched_payment_requests:
|
||||
key = tuple(row[:3])
|
||||
payment_requests[key] = row[3]
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
|
||||
|
||||
if key in payment_requests:
|
||||
ref.payment_request = payment_requests[key]
|
||||
del payment_requests[key] # to avoid duplicate allocation
|
||||
|
||||
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# to fetch matched rows
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.allocated_amount)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
# query to group by reference_doctype, reference_name, outstanding_amount
|
||||
subquery = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(
|
||||
PR.reference_doctype,
|
||||
PR.reference_name,
|
||||
PR.outstanding_amount.as_("allocated_amount"),
|
||||
PR.name.as_("payment_request"),
|
||||
Count("*").as_("count"),
|
||||
)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
)
|
||||
|
||||
# query to fetch matched rows which are single
|
||||
matched_prs = (
|
||||
frappe.qb.from_(subquery)
|
||||
.select(
|
||||
subquery.reference_doctype,
|
||||
subquery.reference_name,
|
||||
subquery.allocated_amount,
|
||||
subquery.payment_request,
|
||||
)
|
||||
.where(subquery.count == 1)
|
||||
.run()
|
||||
)
|
||||
|
||||
return matched_prs if matched_prs else None
|
||||
|
||||
|
||||
def get_references_outstanding_amount(references=None):
|
||||
"""
|
||||
Fetch accurate outstanding amount of `References`.\n
|
||||
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
|
||||
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
|
||||
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
|
||||
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
|
||||
|
||||
return {**refs_with_payment_term, **refs_without_payment_term}
|
||||
|
||||
|
||||
def get_outstanding_of_references_with_payment_term(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have `Payment Term` set.\n
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.payment_term)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.payment_term
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PS = frappe.qb.DocType("Payment Schedule")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PS)
|
||||
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
|
||||
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
|
||||
|
||||
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
outstanding_amounts = {}
|
||||
|
||||
for ref in references:
|
||||
if ref.payment_term:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, None)
|
||||
|
||||
if key not in outstanding_amounts:
|
||||
outstanding_amounts[key] = ref.outstanding_amount
|
||||
|
||||
return outstanding_amounts
|
||||
|
||||
|
||||
def get_payment_request_outstanding_set_in_references(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
|
||||
Example: {payment_request: outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
|
||||
|
||||
if not referenced_payment_requests:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.outstanding_amount)
|
||||
.where(PR.name.isin(referenced_payment_requests))
|
||||
).run()
|
||||
|
||||
return dict(response) if response else None
|
||||
|
||||
|
||||
def validate_inclusive_tax(tax, doc):
|
||||
def _on_previous_row_error(row_range):
|
||||
@@ -2058,7 +2489,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
@@ -2236,6 +2669,8 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
@@ -2385,9 +2820,179 @@ def get_payment_entry(
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
|
||||
|
||||
return pe
|
||||
|
||||
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
reference_payment_requests = {}
|
||||
|
||||
for row in response:
|
||||
key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
if key not in reference_payment_requests:
|
||||
reference_payment_requests[key] = {row.name: row.outstanding_amount}
|
||||
else:
|
||||
reference_payment_requests[key][row.name] = row.outstanding_amount
|
||||
|
||||
return reference_payment_requests
|
||||
|
||||
|
||||
def allocate_open_payment_requests_to_references(references=None, precision=None):
|
||||
"""
|
||||
Allocate unpaid Payment Requests to the references. \n
|
||||
---
|
||||
- Allocation based on below factors
|
||||
- Reference Allocated Amount
|
||||
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
|
||||
- Reference Payment Request's outstanding amount
|
||||
---
|
||||
- Allocation based on below scenarios
|
||||
- Reference's Allocated Amount == Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- This PR will not be allocated further
|
||||
- Reference's Allocated Amount < Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce the PR's outstanding amount by the allocated amount
|
||||
- This PR can be allocated further
|
||||
- Reference's Allocated Amount > Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce Allocated Amount of the reference by the PR's outstanding amount
|
||||
- Create a new row for the remaining amount until the Allocated Amount is 0
|
||||
- Allocate PR if available
|
||||
---
|
||||
- Note:
|
||||
- Priority is given to the first Payment Request of respective references.
|
||||
- Single Reference can have multiple rows.
|
||||
- With Payment Terms or without Payment Terms
|
||||
- With Payment Request or without Payment Request
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# get all unpaid payment requests for the references
|
||||
references_open_payment_requests = get_open_payment_requests_for_references(references)
|
||||
|
||||
if not references_open_payment_requests:
|
||||
return
|
||||
|
||||
if not precision:
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
# to manage new rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
reference_key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
# unpaid payment requests for the reference
|
||||
reference_payment_requests = references_open_payment_requests.get(reference_key)
|
||||
|
||||
if not reference_payment_requests:
|
||||
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
|
||||
continue
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
|
||||
allocated_amount = row.allocated_amount
|
||||
|
||||
# allocate the payment request to the reference and PR's outstanding amount
|
||||
row.payment_request = payment_request
|
||||
|
||||
if pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
# reduce the outstanding amount of the payment request
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
else:
|
||||
# split the reference row to allocate the remaining amount
|
||||
del reference_payment_requests[payment_request]
|
||||
row.allocated_amount = pr_outstanding_amount
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
|
||||
# set the remaining amount to the next row
|
||||
while allocated_amount:
|
||||
# create a new row for the remaining amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(
|
||||
iter(reference_payment_requests.items()), (None, None)
|
||||
)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = payment_request
|
||||
new_row.allocated_amount = min(
|
||||
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
|
||||
)
|
||||
|
||||
if not payment_request or not pr_outstanding_amount:
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
else:
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
|
||||
def update_accounting_dimensions(pe, doc):
|
||||
"""
|
||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"payment_term_outstanding",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"column_break_4",
|
||||
@@ -18,7 +19,9 @@
|
||||
"allocated_amount",
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss",
|
||||
"account"
|
||||
"account",
|
||||
"payment_request",
|
||||
"payment_request_outstanding"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -120,12 +123,33 @@
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Request",
|
||||
"options": "Payment Request"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_term",
|
||||
"fieldname": "payment_term_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"label": "Payment Term Outstanding",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
|
||||
"fieldname": "payment_request_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"is_virtual": 1,
|
||||
"label": "Payment Request Outstanding",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-05 09:44:08.310593",
|
||||
"modified": "2024-09-16 18:11:50.019343",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_request: DF.Link | None
|
||||
payment_request_outstanding: DF.Float
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@property
|
||||
def payment_request_outstanding(self):
|
||||
if not self.payment_request:
|
||||
return
|
||||
|
||||
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")
|
||||
|
||||
@@ -323,6 +323,7 @@ class PaymentReconciliation(Document):
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
"cost_center": inv.cost_center,
|
||||
"remarks": inv.remarks,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1986,13 +1986,15 @@ def make_period_closing_voucher(company, cost_center, posting_date=None, submit=
|
||||
parent_account=parent_account,
|
||||
doctype="Account",
|
||||
)
|
||||
fy = get_fiscal_year(posting_date, company=company)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": fy[2],
|
||||
"company": company,
|
||||
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"amount",
|
||||
"difference_amount",
|
||||
"sec_break1",
|
||||
"remark",
|
||||
"remarks",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"cost_center"
|
||||
@@ -74,12 +74,6 @@
|
||||
"fieldname": "sec_break1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remark",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
@@ -105,12 +99,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remarks",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:34.818530",
|
||||
"modified": "2024-10-29 16:24:43.021230",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
|
||||
@@ -27,7 +27,7 @@ class PaymentReconciliationPayment(Document):
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_row: DF.Data | None
|
||||
reference_type: DF.Link | None
|
||||
remark: DF.SmallText | None
|
||||
remarks: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
}
|
||||
|
||||
if (
|
||||
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
|
||||
frm.doc.status == "Initiated"
|
||||
frm.doc.payment_request_type == "Outward" &&
|
||||
["Initiated", "Partially Paid"].includes(frm.doc.status)
|
||||
) {
|
||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||
frappe.call({
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
"party_details",
|
||||
"party_type",
|
||||
"party",
|
||||
"party_name",
|
||||
"column_break_4",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"transaction_details",
|
||||
"grand_total",
|
||||
"currency",
|
||||
"is_a_subscription",
|
||||
"column_break_18",
|
||||
"currency",
|
||||
"outstanding_amount",
|
||||
"party_account_currency",
|
||||
"subscription_section",
|
||||
"subscription_plans",
|
||||
"bank_account_details",
|
||||
@@ -69,6 +72,7 @@
|
||||
{
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_preview": 1,
|
||||
"label": "Transaction Date"
|
||||
},
|
||||
{
|
||||
@@ -133,7 +137,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "reference_doctype",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_details",
|
||||
@@ -141,12 +146,14 @@
|
||||
"label": "Transaction Details"
|
||||
},
|
||||
{
|
||||
"description": "Amount in customer's currency",
|
||||
"description": "Amount in transaction currency",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -392,19 +399,43 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.docstatus === 1",
|
||||
"description": "Amount in party's bank account currency",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Outstanding Amount",
|
||||
"non_negative": 1,
|
||||
"options": "party_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Party Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-07 16:39:54.288002",
|
||||
"modified": "2024-10-23 12:23:40.117336",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -439,7 +470,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
@@ -18,6 +20,15 @@ from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"POS Invoice",
|
||||
"Fees",
|
||||
]
|
||||
|
||||
|
||||
def _get_payment_gateway_controller(*args, **kwargs):
|
||||
with payment_app_import_guard():
|
||||
@@ -45,6 +56,7 @@ class PaymentRequest(Document):
|
||||
bank_account: DF.Link | None
|
||||
bank_account_no: DF.ReadOnly | None
|
||||
branch_code: DF.ReadOnly | None
|
||||
company: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
email_to: DF.Data | None
|
||||
@@ -56,16 +68,19 @@ class PaymentRequest(Document):
|
||||
mode_of_payment: DF.Link | None
|
||||
mute_email: DF.Check
|
||||
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
||||
outstanding_amount: DF.Currency
|
||||
party: DF.DynamicLink | None
|
||||
party_account_currency: DF.Link | None
|
||||
party_name: DF.Data | None
|
||||
party_type: DF.Link | None
|
||||
payment_account: DF.ReadOnly | None
|
||||
payment_channel: DF.Literal["", "Email", "Phone"]
|
||||
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
|
||||
payment_gateway: DF.ReadOnly | None
|
||||
payment_gateway_account: DF.Link | None
|
||||
payment_order: DF.Link | None
|
||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||
payment_url: DF.Data | None
|
||||
print_format: DF.Literal
|
||||
print_format: DF.Literal[None]
|
||||
project: DF.Link | None
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
@@ -84,7 +99,6 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -100,6 +114,12 @@ class PaymentRequest(Document):
|
||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||
|
||||
def validate_payment_request_amount(self):
|
||||
if self.grand_total == 0:
|
||||
frappe.throw(
|
||||
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
|
||||
title=_("Invalid Amount"),
|
||||
)
|
||||
|
||||
existing_payment_request_amount = flt(
|
||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||
)
|
||||
@@ -147,6 +167,28 @@ class PaymentRequest(Document):
|
||||
).format(self.grand_total, amount)
|
||||
)
|
||||
|
||||
def before_submit(self):
|
||||
if (
|
||||
self.currency != self.party_account_currency
|
||||
and self.party_account_currency == get_company_currency(self.company)
|
||||
):
|
||||
# set outstanding amount in party account currency
|
||||
invoice = frappe.get_value(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
|
||||
as_dict=1,
|
||||
)
|
||||
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
|
||||
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
|
||||
self.outstanding_amount = flt(
|
||||
self.grand_total / grand_total * base_grand_total,
|
||||
self.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
else:
|
||||
self.outstanding_amount = self.grand_total
|
||||
|
||||
def on_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
@@ -262,12 +304,12 @@ class PaymentRequest(Document):
|
||||
return controller.get_payment_url(
|
||||
**{
|
||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||
"title": data.company.encode("utf-8"),
|
||||
"description": self.subject.encode("utf-8"),
|
||||
"title": data.company,
|
||||
"description": self.subject,
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": self.name,
|
||||
"payer_email": self.email_to or frappe.session.user,
|
||||
"payer_name": frappe.safe_encode(data.customer_name),
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
}
|
||||
@@ -275,7 +317,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def set_as_paid(self):
|
||||
if self.payment_channel == "Phone":
|
||||
self.db_set("status", "Paid")
|
||||
self.db_set({"status": "Paid", "outstanding_amount": 0})
|
||||
|
||||
else:
|
||||
payment_entry = self.create_payment_entry()
|
||||
@@ -296,26 +338,32 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
party_account_currency = (
|
||||
self.get("party_account_currency")
|
||||
or ref_doc.get("party_account_currency")
|
||||
or get_account_currency(party_account)
|
||||
)
|
||||
|
||||
party_amount = bank_amount = self.outstanding_amount
|
||||
|
||||
bank_amount = self.grand_total
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
exchange_rate = ref_doc.get("conversion_rate")
|
||||
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
|
||||
|
||||
# outstanding amount is already in Part's account currency
|
||||
payment_entry = get_payment_entry(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
party_amount=party_amount,
|
||||
bank_account=self.payment_account,
|
||||
bank_amount=bank_amount,
|
||||
created_from_payment_request=True,
|
||||
)
|
||||
|
||||
payment_entry.update(
|
||||
{
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_no": self.name, # to prevent validation error
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||
self.reference_doctype, self.reference_name, self.name
|
||||
@@ -323,6 +371,9 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
|
||||
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
|
||||
|
||||
# Update dimensions
|
||||
payment_entry.update(
|
||||
{
|
||||
@@ -331,14 +382,6 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
@@ -429,6 +472,62 @@ class PaymentRequest(Document):
|
||||
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
|
||||
def _allocate_payment_request_to_pe_references(self, references):
|
||||
"""
|
||||
Allocate the Payment Request to the Payment Entry references based on\n
|
||||
- Allocated Amount.
|
||||
- Outstanding Amount of Payment Request.\n
|
||||
Payment Request is doc itself and references are the rows of Payment Entry.
|
||||
"""
|
||||
if len(references) == 1:
|
||||
references[0].payment_request = self.name
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
outstanding_amount = self.outstanding_amount
|
||||
|
||||
# to manage rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
NEW_ROW_ADDED = False
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
if outstanding_amount == 0:
|
||||
if not NEW_ROW_ADDED:
|
||||
break
|
||||
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
continue
|
||||
|
||||
# allocate the payment request to the row
|
||||
row.payment_request = self.name
|
||||
|
||||
if row.allocated_amount <= outstanding_amount:
|
||||
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
else:
|
||||
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
|
||||
row.allocated_amount = outstanding_amount
|
||||
outstanding_amount = 0
|
||||
|
||||
# create a new row without PR for remaining unallocated amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = None
|
||||
new_row.allocated_amount = remaining_allocated_amount
|
||||
|
||||
NEW_ROW_ADDED = True
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def make_payment_request(**args):
|
||||
@@ -436,6 +535,9 @@ def make_payment_request(**args):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
@@ -459,11 +561,15 @@ def make_payment_request(**args):
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
)
|
||||
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Request is already created"))
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
@@ -477,6 +583,13 @@ def make_payment_request(**args):
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
party_type = args.get("party_type") or "Customer"
|
||||
party_account_currency = ref_doc.get("party_account_currency")
|
||||
|
||||
if not party_account_currency:
|
||||
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -485,6 +598,7 @@ def make_payment_request(**args):
|
||||
"payment_channel": gateway_account.get("payment_channel"),
|
||||
"payment_request_type": args.get("payment_request_type"),
|
||||
"currency": ref_doc.currency,
|
||||
"party_account_currency": party_account_currency,
|
||||
"grand_total": grand_total,
|
||||
"mode_of_payment": args.mode_of_payment,
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
@@ -493,9 +607,10 @@ def make_payment_request(**args):
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party_type": party_type,
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -539,9 +654,11 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
grand_total = flt(
|
||||
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
)
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -563,24 +680,20 @@ def get_amount(ref_doc, payment_account=None):
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
"""
|
||||
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
||||
and get the summation of existing paid payment request for Phone payment channel.
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
existing_payment_request_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(grand_total)
|
||||
from `tabPayment Request`
|
||||
where
|
||||
reference_doctype = %s
|
||||
and reference_name = %s
|
||||
and docstatus = 1
|
||||
and (status != 'Paid'
|
||||
or (payment_channel = 'Phone'
|
||||
and status = 'Paid'))
|
||||
""",
|
||||
(ref_dt, ref_dn),
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(Sum(PR.grand_total))
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.run()
|
||||
)
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
@@ -627,41 +740,66 @@ def make_payment_entry(docname):
|
||||
return doc.create_payment_entry(submit=False).as_dict()
|
||||
|
||||
|
||||
def update_payment_req_status(doc, method):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
|
||||
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
"""
|
||||
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
for ref in doc.references:
|
||||
payment_request_name = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": ref.reference_doctype,
|
||||
"reference_name": ref.reference_name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
fields=[
|
||||
"name",
|
||||
"grand_total",
|
||||
"outstanding_amount",
|
||||
"payment_request_type",
|
||||
],
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
payment_request = referenced_payment_requests[ref.payment_request]
|
||||
pr_outstanding = payment_request["outstanding_amount"]
|
||||
|
||||
# update outstanding amount
|
||||
new_outstanding_amount = flt(
|
||||
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
|
||||
precision,
|
||||
)
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
# to handle same payment request for the multiple allocations
|
||||
payment_request["outstanding_amount"] = new_outstanding_amount
|
||||
|
||||
if not cancel and new_outstanding_amount < 0:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
|
||||
).format(ref.payment_request),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
if status != "Paid" and not ref_details.outstanding_amount:
|
||||
status = "Paid"
|
||||
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
|
||||
status = "Partially Paid"
|
||||
elif ref_details.outstanding_amount == ref_details.total_amount:
|
||||
if pay_req_doc.payment_request_type == "Outward":
|
||||
status = "Initiated"
|
||||
elif pay_req_doc.payment_request_type == "Inward":
|
||||
status = "Requested"
|
||||
# update status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
status = "Paid"
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
pay_req_doc.db_set("status", status)
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
@@ -745,3 +883,35 @@ def validate_payment(doc, method=None):
|
||||
doc.reference_docname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
return []
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
return [
|
||||
(
|
||||
pr.name,
|
||||
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||
)
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -15,6 +17,7 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||
|
||||
|
||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||
|
||||
payment_method = [
|
||||
@@ -278,3 +281,246 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
def test_multiple_payment_if_partially_paid_for_same_currency(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 800)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
|
||||
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
|
||||
pi.credit_to = "Creditors - _TC"
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 100 USD -> 5000 INR
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
self.assertEqual(pr.outstanding_amount, 5000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 2000
|
||||
pe.references[0].allocated_amount = 2000
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 3000)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 3000)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
def test_single_payment_with_payment_term_for_same_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
|
||||
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Order",
|
||||
dn=po.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 20000)
|
||||
|
||||
# check 1st payment term
|
||||
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_single_payment_with_payment_term_for_multi_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
|
||||
)
|
||||
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=si.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 200 USD -> 10000 INR
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
self.assertEqual(pr.outstanding_amount, 10000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 10000)
|
||||
|
||||
# check 1st payment term
|
||||
# convert it via dollar and conversion_rate
|
||||
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
|
||||
def test_payment_cancel_process(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 800
|
||||
pe.references[0].allocated_amount = 800
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 200)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# cancelling PE
|
||||
pe.cancel()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Requested")
|
||||
self.assertEqual(pr.outstanding_amount, 1000)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
@@ -19,6 +19,24 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
});
|
||||
},
|
||||
|
||||
fiscal_year: function (frm) {
|
||||
if (frm.doc.fiscal_year) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.get_period_start_end_date",
|
||||
args: {
|
||||
fiscal_year: frm.doc.fiscal_year,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("period_start_date", r.message[0]);
|
||||
frm.set_value("period_end_date", r.message[1]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
|
||||
@@ -6,39 +6,32 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_date",
|
||||
"posting_date",
|
||||
"fiscal_year",
|
||||
"year_start_date",
|
||||
"amended_from",
|
||||
"company",
|
||||
"fiscal_year",
|
||||
"period_start_date",
|
||||
"period_end_date",
|
||||
"amended_from",
|
||||
"column_break1",
|
||||
"closing_account_head",
|
||||
"remarks",
|
||||
"gle_processing_status",
|
||||
"remarks",
|
||||
"error_message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Transaction Date",
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Closing Fiscal Year",
|
||||
"label": "Fiscal Year",
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
@@ -103,16 +96,25 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "year_start_date",
|
||||
"fieldname": "period_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Year Start Date"
|
||||
"label": "Period End Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "period_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Period Start Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-11 20:19:11.810533",
|
||||
"modified": "2024-09-15 17:22:45.291628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
@@ -148,7 +150,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"search_fields": "fiscal_year, period_start_date, period_end_date",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, flt
|
||||
from frappe.utils import add_days, flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
@@ -29,38 +34,399 @@ class PeriodClosingVoucher(AccountsController):
|
||||
error_message: DF.Text | None
|
||||
fiscal_year: DF.Link
|
||||
gle_processing_status: DF.Literal["In Progress", "Completed", "Failed"]
|
||||
posting_date: DF.Date
|
||||
period_end_date: DF.Date
|
||||
period_start_date: DF.Date
|
||||
remarks: DF.SmallText
|
||||
transaction_date: DF.Date | None
|
||||
year_start_date: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account_head()
|
||||
self.validate_posting_date()
|
||||
self.validate_start_and_end_date()
|
||||
self.check_if_previous_year_closed()
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.check_closing_account_type()
|
||||
self.check_closing_account_currency()
|
||||
|
||||
def validate_start_and_end_date(self):
|
||||
self.fy_start_date, self.fy_end_date = frappe.db.get_value(
|
||||
"Fiscal Year", self.fiscal_year, ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
prev_closed_period_end_date = get_previous_closed_period_in_current_year(
|
||||
self.fiscal_year, self.company
|
||||
)
|
||||
valid_start_date = (
|
||||
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else self.fy_start_date
|
||||
)
|
||||
|
||||
if getdate(self.period_start_date) != getdate(valid_start_date):
|
||||
frappe.throw(_("Period Start Date must be {0}").format(formatdate(valid_start_date)))
|
||||
|
||||
if getdate(self.period_start_date) > getdate(self.period_end_date):
|
||||
frappe.throw(_("Period Start Date cannot be greater than Period End Date"))
|
||||
|
||||
if getdate(self.period_end_date) > getdate(self.fy_end_date):
|
||||
frappe.throw(_("Period End Date cannot be greater than Fiscal Year End Date"))
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.fy_start_date, -1)
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
gle_exists_in_previous_year = frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
)
|
||||
if not gle_exists_in_previous_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if not previous_fiscal_year_closed:
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def block_if_future_closing_voucher_exists(self):
|
||||
future_closing_voucher = self.get_future_closing_voucher()
|
||||
if future_closing_voucher and future_closing_voucher[0][0]:
|
||||
action = "cancel" if self.docstatus == 2 else "create"
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
|
||||
).format(action, future_closing_voucher[0][0], self.period_end_date)
|
||||
)
|
||||
|
||||
def get_future_closing_voucher(self):
|
||||
return frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
{"period_end_date": (">", self.period_end_date), "docstatus": 1, "company": self.company},
|
||||
"name",
|
||||
)
|
||||
|
||||
def check_closing_account_type(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
if closing_account_type not in ["Liability", "Equity"]:
|
||||
frappe.throw(
|
||||
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
||||
)
|
||||
|
||||
def check_closing_account_currency(self):
|
||||
account_currency = get_account_currency(self.closing_account_head)
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if account_currency != company_currency:
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
get_opening_entries = False
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
|
||||
):
|
||||
get_opening_entries = True
|
||||
|
||||
self.make_gl_entries(get_opening_entries=get_opening_entries)
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
)
|
||||
if gle_count > 5000:
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.cancel_gl_entries()
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.get_gle_count_in_selected_period() > 5000:
|
||||
frappe.enqueue(
|
||||
make_reverse_gl_entries,
|
||||
process_gl_and_closing_entries,
|
||||
doc=self,
|
||||
timeout=1800,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"The GL Entries and closing balances will be processed in the background, it can take a few minutes."
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_and_closing_entries(self)
|
||||
|
||||
def get_gle_count_in_selected_period(self):
|
||||
return frappe.db.count(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ["between", [self.period_start_date, self.period_end_date]],
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def get_pcv_gl_entries(self):
|
||||
self.pl_accounts_reverse_gle = []
|
||||
self.closing_account_gle = []
|
||||
|
||||
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||
for dimensions, account_balances in pl_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
if balance_in_company_currency and acc != "balances":
|
||||
self.pl_accounts_reverse_gle.append(
|
||||
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||
)
|
||||
|
||||
# closing liability account
|
||||
self.closing_account_gle.append(
|
||||
self.get_gle_for_closing_account(account_balances["balances"], dimensions)
|
||||
)
|
||||
|
||||
return self.pl_accounts_reverse_gle + self.closing_account_gle
|
||||
|
||||
def get_gle_for_pl_account(self, 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": self.company,
|
||||
"posting_date": self.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": self.name,
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"remarks": self.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"posting_date": self.period_end_date,
|
||||
"account": self.closing_account_head,
|
||||
"account_currency": frappe.db.get_value(
|
||||
"Account", self.closing_account_head, "account_currency"
|
||||
),
|
||||
"debit_in_account_currency": balance_in_account_currency
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"debit": 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": self.name,
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"remarks": self.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
def update_default_dimensions(self, gl_entry, dimensions):
|
||||
for i, dimension in enumerate(self.accounting_dimension_fields):
|
||||
gl_entry[dimension] = dimensions[i]
|
||||
|
||||
def get_account_balances_based_on_dimensions(self, report_type):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
self.get_accounting_dimension_fields()
|
||||
acc_bal_dict = frappe._dict()
|
||||
gl_entries = []
|
||||
|
||||
with frappe.db.unbuffered_cursor():
|
||||
gl_entries = self.get_gl_entries_for_current_period(report_type, as_iterator=True)
|
||||
for gle in gl_entries:
|
||||
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||
|
||||
if report_type == "Balance Sheet" and self.is_first_period_closing_voucher():
|
||||
opening_entries = self.get_gl_entries_for_current_period(report_type, only_opening_entries=True)
|
||||
for gle in opening_entries:
|
||||
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||
|
||||
return acc_bal_dict
|
||||
|
||||
def get_accounting_dimension_fields(self):
|
||||
default_dimensions = ["cost_center", "finance_book", "project"]
|
||||
self.accounting_dimension_fields = default_dimensions + get_accounting_dimensions()
|
||||
|
||||
def get_gl_entries_for_current_period(self, report_type, only_opening_entries=False, as_iterator=False):
|
||||
date_condition = ""
|
||||
if only_opening_entries:
|
||||
date_condition = "is_opening = 'Yes'"
|
||||
else:
|
||||
date_condition = f"posting_date BETWEEN '{self.period_start_date}' AND '{self.period_end_date}' and is_opening = 'No'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name,
|
||||
posting_date,
|
||||
account,
|
||||
account_currency,
|
||||
debit_in_account_currency,
|
||||
credit_in_account_currency,
|
||||
debit,
|
||||
credit,
|
||||
{}
|
||||
FROM `tabGL Entry`
|
||||
WHERE
|
||||
{}
|
||||
AND company = %s
|
||||
AND voucher_type != 'Period Closing Voucher'
|
||||
AND EXISTS(SELECT name FROM `tabAccount` WHERE name = account AND report_type = %s)
|
||||
AND is_cancelled = 0
|
||||
""".format(
|
||||
", ".join(self.accounting_dimension_fields),
|
||||
date_condition,
|
||||
),
|
||||
(self.company, report_type),
|
||||
as_dict=1,
|
||||
as_iterator=as_iterator,
|
||||
)
|
||||
|
||||
def set_account_balance_dict(self, gle, acc_bal_dict):
|
||||
key = self.get_key(gle)
|
||||
|
||||
acc_bal_dict.setdefault(key, frappe._dict()).setdefault(
|
||||
gle.account,
|
||||
frappe._dict(
|
||||
{
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"account_currency": gle.account_currency,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
acc_bal_dict[key][gle.account].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||
acc_bal_dict[key][gle.account].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||
acc_bal_dict[key][gle.account].debit += flt(gle.debit)
|
||||
acc_bal_dict[key][gle.account].credit += flt(gle.credit)
|
||||
|
||||
# dimension-wise total balances
|
||||
acc_bal_dict[key].setdefault(
|
||||
"balances",
|
||||
frappe._dict(
|
||||
{
|
||||
"balance_in_account_currency": 0,
|
||||
"balance_in_company_currency": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
balance_in_account_currency = flt(gle.debit_in_account_currency) - flt(gle.credit_in_account_currency)
|
||||
balance_in_company_currency = flt(gle.debit) - flt(gle.credit)
|
||||
|
||||
acc_bal_dict[key]["balances"].balance_in_account_currency += balance_in_account_currency
|
||||
acc_bal_dict[key]["balances"].balance_in_company_currency += balance_in_company_currency
|
||||
|
||||
return acc_bal_dict
|
||||
|
||||
def get_key(self, gle):
|
||||
return tuple([gle.get(dimension) for dimension in self.accounting_dimension_fields])
|
||||
|
||||
def get_account_closing_balances(self):
|
||||
pl_closing_entries = self.get_closing_entries_for_pl_accounts()
|
||||
bs_closing_entries = self.get_closing_entries_for_balance_sheet_accounts()
|
||||
closing_entries_for_closing_account = self.get_closing_entries_for_closing_account()
|
||||
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entries_for_pl_accounts(self):
|
||||
closing_entries = copy.deepcopy(self.pl_accounts_reverse_gle)
|
||||
for d in self.pl_accounts_reverse_gle:
|
||||
# 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 = self.name
|
||||
closing_entries.append(gle_copy)
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entries_for_balance_sheet_accounts(self):
|
||||
closing_entries = []
|
||||
balance_sheet_account_balances = self.get_account_balances_based_on_dimensions(
|
||||
report_type="Balance Sheet"
|
||||
)
|
||||
|
||||
for dimensions, account_balances in balance_sheet_account_balances.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(self.get_closing_entry(acc, balances, dimensions))
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entry(self, account, balances, dimensions):
|
||||
closing_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.period_end_date,
|
||||
"period_closing_voucher": self.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,
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(closing_entry, dimensions)
|
||||
return closing_entry
|
||||
|
||||
def get_closing_entries_for_closing_account(self):
|
||||
closing_entries = copy.deepcopy(self.closing_account_gle)
|
||||
for d in closing_entries:
|
||||
d.period_closing_voucher = self.name
|
||||
|
||||
return closing_entries
|
||||
|
||||
def is_first_period_closing_voucher(self):
|
||||
first_pcv = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
{"company": self.company, "docstatus": 1},
|
||||
"name",
|
||||
order_by="period_end_date",
|
||||
)
|
||||
|
||||
if not first_pcv or first_pcv == self.name:
|
||||
return True
|
||||
|
||||
def cancel_gl_entries(self):
|
||||
if self.get_gle_count_against_current_pcv() > 5000:
|
||||
frappe.enqueue(
|
||||
process_cancellation,
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
@@ -71,341 +437,74 @@ class PeriodClosingVoucher(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
self.delete_closing_entries()
|
||||
|
||||
def validate_future_closing_vouchers(self):
|
||||
if frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
|
||||
)
|
||||
)
|
||||
|
||||
def delete_closing_entries(self):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == self.name
|
||||
).run()
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
if closing_account_type not in ["Liability", "Equity"]:
|
||||
frappe.throw(
|
||||
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(self.closing_account_head)
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if account_currency != company_currency:
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def validate_posting_date(self):
|
||||
validate_fiscal_year(
|
||||
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
|
||||
)
|
||||
|
||||
self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
existing_entry = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(pcv.name)
|
||||
.where(
|
||||
(pcv.posting_date >= self.posting_date)
|
||||
& (pcv.fiscal_year == self.fiscal_year)
|
||||
& (pcv.docstatus == 1)
|
||||
& (pcv.company == self.company)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if existing_entry and existing_entry[0][0]:
|
||||
frappe.throw(
|
||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
||||
existing_entry[0][0], self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.year_start_date, -1)
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
if not frappe.db.exists(
|
||||
def get_gle_count_against_current_pcv(self):
|
||||
return frappe.db.count(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
):
|
||||
return
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def make_gl_entries(self, get_opening_entries=False):
|
||||
gl_entries = self.get_gl_entries()
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if len(gl_entries + closing_entries) > 3000:
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
voucher_name=self.name,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.enqueue(
|
||||
process_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
company=self.company,
|
||||
closing_date=self.posting_date,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries, self.name)
|
||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
|
||||
):
|
||||
closing_entries.append(self.get_closing_entries(acc))
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
|
||||
# pl account
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
# closing liability account
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=False, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_gle_for_pl_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
def get_closing_entries(self, acc):
|
||||
closing_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"period_closing_voucher": self.name,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": flt(acc.debit_in_account_currency),
|
||||
"debit": flt(acc.debit),
|
||||
"credit_in_account_currency": flt(acc.credit_in_account_currency),
|
||||
"credit": flt(acc.credit),
|
||||
},
|
||||
item=acc,
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
for dimension in self.accounting_dimensions:
|
||||
closing_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
return closing_entry
|
||||
|
||||
def update_default_dimensions(self, gl_entry, acc):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
def get_balances_based_on_dimensions(
|
||||
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
|
||||
):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
qb_dimension_fields = ["cost_center", "finance_book", "project"]
|
||||
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
qb_dimension_fields.append(dimension)
|
||||
|
||||
if group_by_account:
|
||||
qb_dimension_fields.append("account")
|
||||
|
||||
account_filters = {
|
||||
"company": self.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
|
||||
if report_type:
|
||||
account_filters.update({"report_type": report_type})
|
||||
|
||||
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
|
||||
|
||||
if not for_aggregation:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
|
||||
"bal_in_account_currency"
|
||||
),
|
||||
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
|
||||
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
|
||||
(Sum(gl_entry.debit)).as_("debit"),
|
||||
(Sum(gl_entry.credit)).as_("credit"),
|
||||
)
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.select(gl_entry[dimension])
|
||||
|
||||
query = query.where(
|
||||
(gl_entry.company == self.company)
|
||||
& (gl_entry.is_cancelled == 0)
|
||||
& (gl_entry.account.isin(accounts))
|
||||
)
|
||||
|
||||
if get_opening_entries:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
| gl_entry.is_opening
|
||||
== "Yes"
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
& gl_entry.is_opening
|
||||
== "No"
|
||||
)
|
||||
|
||||
if for_aggregation:
|
||||
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.groupby(gl_entry[dimension])
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, voucher_name):
|
||||
def process_gl_and_closing_entries(doc):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
gl_entries = doc.get_pcv_gl_entries()
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
|
||||
closing_entries = doc.get_account_closing_balances()
|
||||
make_closing_entries(closing_entries, doc.name, doc.company, doc.period_end_date)
|
||||
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
try:
|
||||
if gl_entries + closing_entries:
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
def process_cancellation(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||
delete_closing_entries(voucher_no)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def delete_closing_entries(voucher_no):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == voucher_no
|
||||
).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_period_start_end_date(fiscal_year, company):
|
||||
fy_start_date, fy_end_date = frappe.db.get_value(
|
||||
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
|
||||
)
|
||||
prev_closed_period_end_date = get_previous_closed_period_in_current_year(fiscal_year, company)
|
||||
period_start_date = (
|
||||
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else fy_start_date
|
||||
)
|
||||
return period_start_date, fy_end_date
|
||||
|
||||
|
||||
def get_previous_closed_period_in_current_year(fiscal_year, company):
|
||||
prev_closed_period_end_date = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"company": company,
|
||||
"fiscal_year": fiscal_year,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname=["period_end_date"],
|
||||
order_by="period_end_date desc",
|
||||
)
|
||||
return prev_closed_period_end_date
|
||||
|
||||
@@ -317,16 +317,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
repost_doc.posting_date = today()
|
||||
repost_doc.save()
|
||||
|
||||
def make_period_closing_voucher(self, posting_date=None, submit=True):
|
||||
def make_period_closing_voucher(self, posting_date, submit=True):
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
fy = get_fiscal_year(posting_date, company="Test PCV Company")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": fy[2],
|
||||
"company": "Test PCV Company",
|
||||
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
) {
|
||||
reset_values(frm);
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||
() => frm.trigger("set_opening_amounts"),
|
||||
() => frm.trigger("get_pos_invoices"),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -57,6 +57,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
}
|
||||
|
||||
onload_post_render(frm) {
|
||||
super.onload_post_render();
|
||||
this.pos_profile(frm);
|
||||
}
|
||||
|
||||
|
||||
@@ -862,6 +862,7 @@ def get_item_group(pos_profile):
|
||||
if pos_profile.get("item_groups"):
|
||||
# Get items based on the item groups defined in the POS profile
|
||||
for row in pos_profile.get("item_groups"):
|
||||
item_groups.append(row.item_group)
|
||||
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
@@ -1131,6 +1131,12 @@ class TestPricingRule(FrappeTestCase):
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
|
||||
so.items[0].qty = 1
|
||||
del so.items[-1]
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
@@ -657,6 +657,9 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
"qty": qty,
|
||||
@@ -725,14 +728,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
def validate_coupon_code(coupon_name):
|
||||
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
||||
|
||||
if coupon.valid_from:
|
||||
if coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto:
|
||||
if coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.used >= coupon.maximum_use:
|
||||
if coupon.valid_from and coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto and coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
|
||||
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"payment_terms_template",
|
||||
"sales_partner",
|
||||
"sales_person",
|
||||
"show_remarks",
|
||||
"based_on_payment_terms",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
@@ -390,10 +391,16 @@
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -70,6 +70,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
sales_person: DF.Link | None
|
||||
sender: DF.Link | None
|
||||
show_net_values_in_party_account: DF.Check
|
||||
show_remarks: DF.Check
|
||||
start_date: DF.Date | None
|
||||
subject: DF.Data | None
|
||||
terms_and_conditions: DF.Link | None
|
||||
@@ -187,6 +188,7 @@ def get_common_filters(doc):
|
||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||
"show_remarks": doc.show_remarks,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
pricing_rule_fields = [
|
||||
"apply_on",
|
||||
@@ -162,22 +164,50 @@ class PromotionalScheme(Document):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
transaction_exists = False
|
||||
docnames = []
|
||||
invalid_pricing_rule = self.get_invalid_pricing_rules()
|
||||
|
||||
# If user has changed applicable for
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
if not invalid_pricing_rule:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
if frappe.db.exists(
|
||||
"Pricing Rule Detail",
|
||||
{
|
||||
"pricing_rule": ["in", invalid_pricing_rule],
|
||||
"docstatus": ["<", 2],
|
||||
},
|
||||
):
|
||||
raise_for_transaction_exists(self.name)
|
||||
|
||||
for docname in docnames:
|
||||
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
|
||||
raise_for_transaction_exists(self.name)
|
||||
for doc in invalid_pricing_rule:
|
||||
frappe.delete_doc("Pricing Rule", doc)
|
||||
|
||||
if docnames and not transaction_exists:
|
||||
for docname in docnames:
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
frappe.msgprint(
|
||||
_("The following invalid Pricing Rules are deleted:")
|
||||
+ "<br><br><ul><li>"
|
||||
+ "</li><li>".join(invalid_pricing_rule)
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
def get_invalid_pricing_rules(self):
|
||||
pr = frappe.qb.DocType("Pricing Rule")
|
||||
conditions = []
|
||||
conditions.append(pr.promotional_scheme == self.name)
|
||||
|
||||
if self.applicable_for:
|
||||
applicable_for = frappe.scrub(self.applicable_for)
|
||||
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
|
||||
|
||||
conditions.append(
|
||||
(IfNull(pr.applicable_for, "") != self.applicable_for)
|
||||
| (
|
||||
(IfNull(pr.applicable_for, "") == self.applicable_for)
|
||||
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
|
||||
)
|
||||
)
|
||||
else:
|
||||
conditions.append(IfNull(pr.applicable_for, "") != "")
|
||||
|
||||
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
|
||||
@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_change_applicable_for_values_in_promotional_scheme(self):
|
||||
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
|
||||
)
|
||||
self.assertTrue(len(price_rules), 2)
|
||||
|
||||
ps.set("customer", [])
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule",
|
||||
filters={
|
||||
"promotional_scheme": ps.name,
|
||||
"applicable_for": "Customer",
|
||||
"customer": "_Test Customer",
|
||||
},
|
||||
)
|
||||
self.assertEqual(price_rules, [])
|
||||
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||
|
||||
def test_min_max_amount_configuration(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.price_discount_slabs[0].min_amount = 10
|
||||
|
||||
@@ -31,6 +31,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onload() {
|
||||
@@ -335,7 +342,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
party_type: "Supplier",
|
||||
account: this.frm.doc.credit_to,
|
||||
price_list: this.frm.doc.buying_price_list,
|
||||
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template),
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
@@ -506,13 +515,6 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.set_query("expense_account", "items", function (doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
|
||||
cur_frm.set_query("wip_composite_asset", "items", function () {
|
||||
return {
|
||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||
@@ -561,11 +563,12 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm.custom_make_buttons = {
|
||||
"Purchase Invoice": "Return / Debit Note",
|
||||
"Payment Entry": "Payment",
|
||||
"Landed Cost Voucher": function () {
|
||||
frm.trigger("create_landed_cost_voucher");
|
||||
},
|
||||
};
|
||||
|
||||
if (frm.doc.update_stock) {
|
||||
frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher";
|
||||
}
|
||||
|
||||
frm.set_query("additional_discount_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -607,20 +610,6 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
});
|
||||
},
|
||||
|
||||
create_landed_cost_voucher: function (frm) {
|
||||
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
|
||||
lcv.company = frm.doc.company;
|
||||
|
||||
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice");
|
||||
lcv_receipt.receipt_document_type = "Purchase Invoice";
|
||||
lcv_receipt.receipt_document = frm.doc.name;
|
||||
lcv_receipt.supplier = frm.doc.supplier;
|
||||
lcv_receipt.grand_total = frm.doc.grand_total;
|
||||
lcv.purchase_receipts = [lcv_receipt];
|
||||
|
||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
||||
},
|
||||
|
||||
add_custom_buttons: function (frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(
|
||||
@@ -645,6 +634,32 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
|
||||
frm.add_custom_button(
|
||||
__("Landed Cost Voucher"),
|
||||
() => {
|
||||
frm.events.make_lcv(frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
make_lcv(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
|
||||
args: {
|
||||
doctype: frm.doc.doctype,
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
|
||||
@@ -1134,12 +1134,14 @@
|
||||
"label": "Payment Terms"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_terms_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_schedule",
|
||||
"fieldtype": "Table",
|
||||
"label": "Payment Schedule",
|
||||
@@ -1631,7 +1633,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified": "2024-10-25 18:13:01.944477",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -1593,7 +1593,11 @@ class PurchaseInvoice(BuyingController):
|
||||
for proj, value in projects.items():
|
||||
res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
||||
current_purchase_cost = res and res[0][0] or 0
|
||||
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
# frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
project_doc = frappe.get_doc("Project", proj)
|
||||
project_doc.total_purchase_cost = current_purchase_cost + value
|
||||
project_doc.calculate_gross_margin()
|
||||
project_doc.db_update()
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
|
||||
@@ -2292,6 +2292,24 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_last_purchase_rate(self):
|
||||
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
|
||||
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 200)
|
||||
|
||||
pi2.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi1.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -505,7 +505,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -974,7 +975,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified": "2024-10-28 15:06:19.246141",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -46,8 +46,8 @@ class RepostAccountingLedger(Document):
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
order_by="period_end_date desc",
|
||||
pluck="period_end_date",
|
||||
limit=1,
|
||||
)
|
||||
or None
|
||||
|
||||
@@ -129,13 +129,15 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
fy = get_fiscal_year(today(), company=self.company)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": today(),
|
||||
"company": self.company,
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -339,6 +339,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
account: this.frm.doc.debit_to,
|
||||
price_list: this.frm.doc.selling_price_list,
|
||||
pos_profile: pos_profile,
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
|
||||
@@ -297,8 +297,11 @@ class SalesInvoice(SellingController):
|
||||
self.update_current_stock()
|
||||
self.validate_delivery_note()
|
||||
|
||||
is_deferred_invoice = any(d.get("enable_deferred_revenue") for d in self.get("items"))
|
||||
|
||||
# validate service stop date to lie in between start and end date
|
||||
validate_service_stop_date(self)
|
||||
if is_deferred_invoice:
|
||||
validate_service_stop_date(self)
|
||||
|
||||
if not self.is_opening:
|
||||
self.is_opening = "No"
|
||||
@@ -1359,14 +1362,15 @@ class SalesInvoice(SellingController):
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
if not asset.status == "Fully Depreciated":
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
@@ -1730,9 +1734,11 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
def update_project(self):
|
||||
if self.project:
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
for p in unique_projects:
|
||||
project = frappe.get_doc("Project", p)
|
||||
project.update_billed_amount()
|
||||
project.calculate_gross_margin()
|
||||
project.db_update()
|
||||
|
||||
def verify_payment_amount_is_positive(self):
|
||||
@@ -2123,7 +2129,7 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
"doctype": "Sales Team",
|
||||
"field_map": {"incentives": "incentives"},
|
||||
|
||||
@@ -1995,7 +1995,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
# 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)
|
||||
self.assertEqual(sales_order.advance_paid, 300.0)
|
||||
|
||||
# check outstanding after advance allocation
|
||||
self.assertEqual(
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"options": "Project",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
@@ -927,7 +928,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified": "2024-10-28 15:06:40.980995",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
@@ -414,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
"""
|
||||
|
||||
if party_type == "Supplier":
|
||||
return []
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
conditions = []
|
||||
@@ -511,7 +514,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
@@ -535,7 +538,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
group_by="payment_type",
|
||||
)
|
||||
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
@@ -553,18 +556,18 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
|
||||
or 0.0
|
||||
)
|
||||
net_total += inv.tax_withholding_net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
@@ -607,8 +610,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
|
||||
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
)
|
||||
@@ -631,9 +632,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
)
|
||||
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
||||
|
||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
||||
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
||||
total_invoiced_amt = (
|
||||
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
||||
)
|
||||
|
||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
||||
@@ -642,6 +646,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
return tcs_amount
|
||||
|
||||
|
||||
def get_advance_adjusted_in_invoice(inv):
|
||||
advances_adjusted = 0
|
||||
for row in inv.get("advances", []):
|
||||
advances_adjusted += row.allocated_amount
|
||||
|
||||
return advances_adjusted
|
||||
|
||||
|
||||
def get_invoice_total_without_tcs(inv, tax_details):
|
||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||
|
||||
@@ -121,6 +121,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self):
|
||||
invoices = []
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance TDS Category"
|
||||
)
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=1000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=6000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 800)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
@@ -210,6 +250,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tcs_on_allocated_advance_payments(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
|
||||
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
|
||||
advances = si.get_advance_entries()
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
vouchers.append(si)
|
||||
|
||||
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
|
||||
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
|
||||
self.assertEqual(tcs_charged, 2000)
|
||||
|
||||
# cancel invoice and payments to avoid clashing
|
||||
for d in reversed(vouchers):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
|
||||
@@ -7,7 +7,9 @@ from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
@@ -360,6 +362,107 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
pe.cancel()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
so2 = self.create_sales_order()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
# Allocation payment against Sales Order
|
||||
pe.paid_amount = 260
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so1.doctype, "reference_name": so1.name, "allocated_amount": 150},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so2.doctype, "reference_name": so2.name, "allocated_amount": 110},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so1.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
so2.reload()
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [(x.reference_name, x.allocated_amount) for x in unreconcile.allocations]
|
||||
self.assertListEqual(allocations, [(so1.name, 150), (so2.name, 110)])
|
||||
# unreconcile so2
|
||||
unreconcile.remove(unreconcile.allocations[0])
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so1.reload()
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
self.enable_advance_as_liability()
|
||||
so = self.create_sales_order()
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_amount = 1000
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 1000},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.insert().submit()
|
||||
|
||||
pr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Reconciliation",
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": so.customer,
|
||||
}
|
||||
)
|
||||
accounts = get_party_account("Customer", so.customer, so.company, True)
|
||||
pr.receivable_payable_account = accounts[0]
|
||||
pr.default_advance_account = accounts[1]
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:UNREC-{#####}",
|
||||
"creation": "2023-08-22 10:26:34.421423",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -58,11 +56,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified": "2024-10-10 12:03:50.022444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -37,13 +37,14 @@ def make_gl_entries(
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@@ -116,17 +117,16 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
Account = frappe.qb.DocType("Account")
|
||||
disabled_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
disabled_accounts = (
|
||||
frappe.qb.from_(Account)
|
||||
.where(Account.name.isin(accounts) & Account.disabled == 1)
|
||||
.select(Account.name, Account.disabled)
|
||||
).run(as_dict=True)
|
||||
|
||||
if disabled_accounts:
|
||||
used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts]))
|
||||
if used_disabled_accounts:
|
||||
account_list = "<br>"
|
||||
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
|
||||
account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts])
|
||||
frappe.throw(
|
||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||
title=_("Disabled Account Selected"),
|
||||
@@ -708,7 +708,7 @@ def validate_against_pcv(is_opening, posting_date, company):
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
|
||||
@@ -881,16 +881,17 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
|
||||
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.party, Abs(Sum(ple.amount).as_("amount")))
|
||||
.where(
|
||||
(ple.party_type.isin(party_type))
|
||||
& (ple.amount < 0)
|
||||
& (ple.against_voucher_no == ple.voucher_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
.inner_join(acc)
|
||||
.on(ple.account == acc.name)
|
||||
.select(ple.party)
|
||||
.where((ple.party_type.isin(party_type)) & (acc.account_type == account_type) & (ple.delinked == 0))
|
||||
.groupby(ple.party)
|
||||
)
|
||||
|
||||
@@ -909,9 +910,32 @@ def get_partywise_advanced_payment_amount(
|
||||
if invoice_doctypes := frappe.get_hooks("invoice_doctypes"):
|
||||
query = query.where(ple.voucher_type.notin(invoice_doctypes))
|
||||
|
||||
data = query.run()
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
# Get advance amount from Receivable / Payable Account
|
||||
party_ledger = query.select(Abs(Sum(ple.amount).as_("amount")))
|
||||
party_ledger = party_ledger.where(ple.amount < 0)
|
||||
party_ledger = party_ledger.where(ple.against_voucher_no == ple.voucher_no)
|
||||
party_ledger = party_ledger.where(
|
||||
acc.root_type == ("Liability" if account_type == "Payable" else "Asset")
|
||||
)
|
||||
|
||||
data = party_ledger.run()
|
||||
data = frappe._dict(data or {})
|
||||
|
||||
# Get advance amount from Advance Account
|
||||
advance_ledger = query.select(Sum(ple.amount).as_("amount"), ple.account)
|
||||
advance_ledger = advance_ledger.where(
|
||||
acc.root_type == ("Asset" if account_type == "Payable" else "Liability")
|
||||
)
|
||||
advance_ledger = advance_ledger.groupby(ple.account)
|
||||
advance_ledger = advance_ledger.having(Sum(ple.amount) < 0)
|
||||
|
||||
advance_data = advance_ledger.run()
|
||||
|
||||
for row in advance_data:
|
||||
data.setdefault(row[0], 0)
|
||||
data[row[0]] += abs(row[1])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_default_contact(doctype: str, name: str) -> str | None:
|
||||
|
||||
@@ -385,6 +385,7 @@ class ReceivablePayableReport:
|
||||
self.delivery_notes = frappe._dict()
|
||||
|
||||
# delivery note link inside sales invoice
|
||||
# nosemgrep
|
||||
si_against_dn = frappe.db.sql(
|
||||
"""
|
||||
select parent, delivery_note
|
||||
@@ -400,6 +401,7 @@ class ReceivablePayableReport:
|
||||
if d.delivery_note:
|
||||
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
|
||||
|
||||
# nosemgrep
|
||||
dn_against_si = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, against_sales_invoice
|
||||
@@ -417,13 +419,16 @@ class ReceivablePayableReport:
|
||||
def get_invoice_details(self):
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.account_type == "Receivable":
|
||||
# nosemgrep
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
from `tabSales Invoice`
|
||||
where posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
)
|
||||
for d in si_list:
|
||||
@@ -431,6 +436,7 @@ class ReceivablePayableReport:
|
||||
|
||||
# Get Sales Team
|
||||
if self.filters.show_sales_person:
|
||||
# nosemgrep
|
||||
sales_team = frappe.db.sql(
|
||||
"""
|
||||
select parent, sales_person
|
||||
@@ -445,25 +451,33 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
if self.account_type == "Payable":
|
||||
# nosemgrep
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabPurchase Invoice`
|
||||
where posting_date <= %s
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
):
|
||||
self.invoice_details.setdefault(pi.name, pi)
|
||||
|
||||
# Invoices booked via Journal Entries
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabJournal Entry`
|
||||
where posting_date <= %s
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -472,6 +486,8 @@ class ReceivablePayableReport:
|
||||
self.invoice_details.setdefault(je.name, je)
|
||||
|
||||
def set_party_details(self, row):
|
||||
if not row.party:
|
||||
return
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
@@ -496,6 +512,7 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_payment_terms(self, row):
|
||||
# build payment_terms for row
|
||||
# nosemgrep
|
||||
payment_terms_details = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -505,7 +522,8 @@ class ReceivablePayableReport:
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = %s
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
""",
|
||||
row.voucher_no,
|
||||
@@ -708,6 +726,7 @@ class ReceivablePayableReport:
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
filters = {
|
||||
"posting_date": ("<=", self.filters.report_date),
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"company": self.filters.company,
|
||||
@@ -815,6 +834,7 @@ class ReceivablePayableReport:
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
|
||||
# nosemgrep
|
||||
records = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, parenttype
|
||||
|
||||
@@ -36,8 +36,9 @@ def get_group_by_asset_category_data(filters):
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
)
|
||||
# Update row with corresponding asset data
|
||||
|
||||
row.update(
|
||||
next(
|
||||
asset
|
||||
@@ -111,13 +112,24 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date <= %(to_date)s
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.asset_category
|
||||
@@ -179,13 +191,24 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date <= %(to_date)s
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.name
|
||||
@@ -217,6 +240,7 @@ def get_group_by_asset_data(filters):
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
@@ -445,6 +469,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Capitalized Asset"),
|
||||
"fieldname": "cost_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
|
||||
@@ -122,13 +122,13 @@ def get_provisional_profit_loss(
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
total_assets = flt(asset[0].get(key))
|
||||
total_assets = flt(asset[-2].get(key))
|
||||
effective_liability = 0.00
|
||||
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
if liability and liability[-1] == {}:
|
||||
effective_liability += flt(liability[-2].get(key))
|
||||
if equity and equity[-1] == {}:
|
||||
effective_liability += flt(equity[-2].get(key))
|
||||
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||
@@ -195,9 +195,9 @@ def get_report_summary(
|
||||
key = period if consolidated else period.key
|
||||
if asset:
|
||||
net_asset += asset[-2].get(key)
|
||||
if liability:
|
||||
if liability and liability[-1] == {}:
|
||||
net_liability += liability[-2].get(key)
|
||||
if equity:
|
||||
if equity and equity[-1] == {}:
|
||||
net_equity += equity[-2].get(key)
|
||||
if provisional_profit_loss:
|
||||
net_provisional_profit_loss += provisional_profit_loss.get(key)
|
||||
|
||||
@@ -47,7 +47,7 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
|
||||
@@ -469,10 +469,13 @@ def update_parent_account_names(accounts):
|
||||
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
account_key = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
name_to_account_map[d.name] = account_name
|
||||
account_key = d.account_name
|
||||
|
||||
d.account_key = account_key
|
||||
|
||||
name_to_account_map[d.name] = account_key
|
||||
|
||||
for account in accounts:
|
||||
if account.parent_account:
|
||||
@@ -505,33 +508,26 @@ def get_subsidiary_companies(company):
|
||||
|
||||
def get_accounts(root_type, companies):
|
||||
accounts = []
|
||||
added_accounts = []
|
||||
|
||||
for company in companies:
|
||||
for account in frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
):
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
|
||||
if account_key not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account_key)
|
||||
accounts.extend(
|
||||
frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
)
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
@@ -770,15 +766,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
def filter_accounts(accounts, depth=10):
|
||||
parent_children_map = {}
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[account_name] = d
|
||||
added_accounts = []
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
for d in accounts:
|
||||
if d.account_key in added_accounts:
|
||||
continue
|
||||
|
||||
added_accounts.append(d.account_key)
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[d.account_key] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account_name or None, []).append(d)
|
||||
|
||||
filtered_accounts = []
|
||||
|
||||
@@ -790,7 +788,7 @@ def filter_accounts(accounts, depth=10):
|
||||
for child in children:
|
||||
child.indent = level
|
||||
filtered_accounts.append(child)
|
||||
add_to_list(child.name, level + 1)
|
||||
add_to_list(child.account_key, level + 1)
|
||||
|
||||
add_to_list(None, 0)
|
||||
|
||||
|
||||
@@ -122,21 +122,24 @@ class Deferred_Item:
|
||||
"""
|
||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||
"""
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
if (
|
||||
self.service_start_date != self.service_end_date
|
||||
and add_days(self.last_entry_date, 1) < self.service_end_date
|
||||
):
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.service_end_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
|
||||
@@ -9,6 +9,7 @@ import re
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -181,12 +182,12 @@ def get_data(
|
||||
company,
|
||||
period_list[0]["year_start_date"] if only_current_fiscal_year else None,
|
||||
period_list[-1]["to_date"],
|
||||
root.lft,
|
||||
root.rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root.lft,
|
||||
root.rgt,
|
||||
root_type=root_type,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
@@ -419,93 +420,78 @@ def set_gl_entries_by_account(
|
||||
company,
|
||||
from_date,
|
||||
to_date,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
root_type=None,
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
|
||||
account_filters = {
|
||||
"company": company,
|
||||
"is_group": 0,
|
||||
"lft": (">=", root_lft),
|
||||
"rgt": ("<=", root_rgt),
|
||||
}
|
||||
|
||||
if root_type:
|
||||
account_filters.update(
|
||||
{
|
||||
"root_type": root_type,
|
||||
}
|
||||
# For balance sheet
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
if not from_date and not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": filters.company,
|
||||
"period_end_date": ("<", filters["period_start_date"]),
|
||||
},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_period_closing_voucher:
|
||||
gl_entries += get_accounting_entries(
|
||||
"Account Closing Balance",
|
||||
from_date,
|
||||
to_date,
|
||||
filters,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
root_type,
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
|
||||
accounts_list = frappe.db.get_all(
|
||||
"Account",
|
||||
filters=account_filters,
|
||||
pluck="name",
|
||||
gl_entries += get_accounting_entries(
|
||||
"GL Entry",
|
||||
from_date,
|
||||
to_date,
|
||||
filters,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
root_type,
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
)
|
||||
|
||||
if accounts_list:
|
||||
# For balance sheet
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
if not from_date and not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": filters.company,
|
||||
"posting_date": ("<", filters["period_start_date"]),
|
||||
},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_period_closing_voucher:
|
||||
gl_entries += get_accounting_entries(
|
||||
"Account Closing Balance",
|
||||
from_date,
|
||||
to_date,
|
||||
accounts_list,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].posting_date, 1)
|
||||
ignore_opening_entries = True
|
||||
if filters and filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
gl_entries += get_accounting_entries(
|
||||
"GL Entry",
|
||||
from_date,
|
||||
to_date,
|
||||
accounts_list,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
)
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
return gl_entries_by_account
|
||||
return gl_entries_by_account
|
||||
|
||||
|
||||
def get_accounting_entries(
|
||||
doctype,
|
||||
from_date,
|
||||
to_date,
|
||||
accounts,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
root_type=None,
|
||||
ignore_closing_entries=None,
|
||||
period_closing_voucher=None,
|
||||
ignore_opening_entries=False,
|
||||
):
|
||||
@@ -535,13 +521,30 @@ def get_accounting_entries(
|
||||
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
|
||||
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
query = query.where(gl_entry.account.isin(accounts))
|
||||
|
||||
if (root_lft and root_rgt) or root_type:
|
||||
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
entries = query.run(as_dict=True)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
acc = frappe.qb.DocType("Account")
|
||||
exists_query = (
|
||||
frappe.qb.from_(acc).select(acc.name).where(acc.name == gl_entry.account).where(acc.is_group == 0)
|
||||
)
|
||||
if root_lft and root_rgt:
|
||||
exists_query = exists_query.where(acc.lft >= root_lft).where(acc.rgt <= root_rgt)
|
||||
|
||||
if root_type:
|
||||
exists_query = exists_query.where(acc.root_type == root_type)
|
||||
|
||||
return exists_query
|
||||
|
||||
|
||||
def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
|
||||
gl_entry = frappe.qb.DocType(doctype)
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
@@ -348,10 +348,18 @@ def get_accounts_with_children(accounts):
|
||||
return frappe.qb.from_(doctype).select(doctype.name).where(Criterion.any(conditions)).run(pluck=True)
|
||||
|
||||
|
||||
def set_bill_no(gl_entries):
|
||||
inv_details = get_supplier_invoice_details()
|
||||
for gl in gl_entries:
|
||||
gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "")
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
totals_dict = get_totals_dict()
|
||||
|
||||
set_bill_no(gl_entries)
|
||||
|
||||
gle_map = initialize_gle_map(gl_entries, filters, totals_dict)
|
||||
|
||||
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict)
|
||||
@@ -539,7 +547,6 @@ def get_account_type_map(company):
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
inv_details = get_supplier_invoice_details()
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
@@ -549,7 +556,6 @@ def get_result_as_list(data, filters):
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
d["bill_no"] = inv_details.get(d.get("against_voucher"), "")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ class PaymentLedger:
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
@@ -194,7 +195,7 @@ class PaymentLedger:
|
||||
label=_("Amount"),
|
||||
fieldname="amount",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
options=company_currency,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
|
||||
"""select distinct expense_account
|
||||
from `tabPurchase Invoice Item` where docstatus = 1
|
||||
and (expense_account is not null and expense_account != '')
|
||||
and parenttype='Purchase Invoice'
|
||||
and parent in (%s) order by expense_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple([inv.name for inv in invoice_list]),
|
||||
@@ -451,7 +452,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
"""
|
||||
select parent, expense_account, sum(base_net_amount) as amount
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
group by parent, expense_account
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
@@ -522,7 +523,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
"""
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
|
||||
@@ -526,7 +526,8 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
|
||||
tax_details = frappe.db.sql(
|
||||
"""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head"""
|
||||
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
|
||||
group by parent, account_head"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
|
||||
@@ -94,12 +94,6 @@ def get_data(filters):
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
min_lft, max_rgt = frappe.db.sql(
|
||||
"""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""",
|
||||
(filters.company,),
|
||||
)[0]
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(filters)
|
||||
@@ -112,10 +106,10 @@ def get_data(filters):
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
min_lft,
|
||||
max_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
@@ -150,9 +144,9 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
@@ -168,8 +162,8 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
if getdate(last_period_closing_voucher[0].posting_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].posting_date, 1)
|
||||
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
gle += get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
|
||||
)
|
||||
|
||||
@@ -326,6 +326,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
|
||||
|
||||
if join_required:
|
||||
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
|
||||
query = query.where(child_doc.parenttype == doctype)
|
||||
query = query.distinct()
|
||||
|
||||
if parent_doc.get_table_name() != "tabJournal Entry":
|
||||
|
||||
@@ -87,6 +87,22 @@ class AccountsTestMixin:
|
||||
"parent_account": "Bank Accounts - " + abbr,
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "advance_received",
|
||||
"account_name": "Advance Received",
|
||||
"parent_account": "Current Liabilities - " + abbr,
|
||||
"account_type": "Receivable",
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
{
|
||||
"attribute_name": "advance_paid",
|
||||
"account_name": "Advance Paid",
|
||||
"parent_account": "Current Assets - " + abbr,
|
||||
"account_type": "Payable",
|
||||
}
|
||||
),
|
||||
]
|
||||
for acc in other_accounts:
|
||||
acc_name = acc.account_name + " - " + abbr
|
||||
@@ -101,9 +117,31 @@ class AccountsTestMixin:
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_acc.account_type = acc.get("account_type", None)
|
||||
new_acc.save()
|
||||
setattr(self, acc.attribute_name, new_acc.name)
|
||||
|
||||
self.identify_default_warehouses()
|
||||
|
||||
def enable_advance_as_liability(self):
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.book_advance_payments_in_separate_party_account = True
|
||||
company.default_advance_received_account = self.advance_received
|
||||
company.default_advance_paid_account = self.advance_paid
|
||||
company.save()
|
||||
|
||||
def disable_advance_as_liability(self):
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.book_advance_payments_in_separate_party_account = False
|
||||
company.default_advance_paid_account = company.default_advance_received_account = None
|
||||
company.save()
|
||||
|
||||
def identify_default_warehouses(self):
|
||||
for w in frappe.db.get_all(
|
||||
"Warehouse", filters={"company": self.company}, fields=["name", "warehouse_name"]
|
||||
):
|
||||
setattr(self, "warehouse_" + w.warehouse_name.lower().strip().replace(" ", "_"), w.name)
|
||||
|
||||
def create_usd_receivable_account(self):
|
||||
account_name = "Debtors USD"
|
||||
if not frappe.db.get_value(
|
||||
|
||||
@@ -474,10 +474,14 @@ def reconcile_against_document(
|
||||
doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
frappe.flags.ignore_party_validation = True
|
||||
|
||||
# For payments with `Advance` in separate account feature enabled, only new ledger entries are posted for each reference.
|
||||
# No need to cancel/delete payment ledger entries
|
||||
# When Advance is allocated from an Order to an Invoice
|
||||
# whole ledger must be reposted
|
||||
repost_whole_ledger = any([x.voucher_detail_no for x in entries])
|
||||
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
|
||||
doc.make_advance_gl_entries(cancel=1)
|
||||
if repost_whole_ledger:
|
||||
doc.make_gl_entries(cancel=1)
|
||||
else:
|
||||
doc.make_advance_gl_entries(cancel=1)
|
||||
else:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
|
||||
@@ -511,9 +515,14 @@ def reconcile_against_document(
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
|
||||
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
|
||||
# both ledgers must be posted to for `Advance` in separate account feature
|
||||
# TODO: find a more efficient way post only for the new linked vouchers
|
||||
doc.make_advance_gl_entries()
|
||||
# When Advance is allocated from an Order to an Invoice
|
||||
# whole ledger must be reposted
|
||||
if repost_whole_ledger:
|
||||
doc.make_gl_entries()
|
||||
else:
|
||||
# both ledgers must be posted to for `Advance` in separate account feature
|
||||
# TODO: find a more efficient way post only for the new linked vouchers
|
||||
doc.make_advance_gl_entries()
|
||||
else:
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
@@ -1538,12 +1547,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
return matched
|
||||
|
||||
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None, accounts=None):
|
||||
stock_accounts = [
|
||||
d.name
|
||||
for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0})
|
||||
]
|
||||
if voucher_type and voucher_no:
|
||||
|
||||
if accounts:
|
||||
stock_accounts = [row.account for row in accounts if row.account in stock_accounts]
|
||||
|
||||
elif voucher_type and voucher_no:
|
||||
if voucher_type == "Journal Entry":
|
||||
stock_accounts = [
|
||||
d.account
|
||||
@@ -1956,6 +1969,7 @@ class QueryPaymentLedger:
|
||||
ple.cost_center.as_("cost_center"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
ple.remarks,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
@@ -2018,6 +2032,7 @@ class QueryPaymentLedger:
|
||||
Table("vouchers").due_date,
|
||||
Table("vouchers").currency,
|
||||
Table("vouchers").cost_center.as_("cost_center"),
|
||||
Table("vouchers").remarks,
|
||||
)
|
||||
.where(Criterion.all(filter_on_outstanding_amount))
|
||||
)
|
||||
|
||||
@@ -119,6 +119,7 @@ class Asset(AccountsController):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_precision()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
self.validate_item()
|
||||
@@ -306,6 +307,15 @@ class Asset(AccountsController):
|
||||
title=_("Missing Finance Book"),
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation, float_precision
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
@@ -471,6 +481,9 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
if not depr_schedule:
|
||||
@@ -790,14 +803,19 @@ class Asset(AccountsController):
|
||||
args.get("value_after_depreciation")
|
||||
)
|
||||
else:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
value = flt(args.get("expected_value_after_useful_life")) / (
|
||||
flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
|
||||
)
|
||||
|
||||
depreciation_rate = math.pow(
|
||||
value,
|
||||
1.0
|
||||
/ (
|
||||
(
|
||||
flt(args.get("total_number_of_depreciations"), 2)
|
||||
(
|
||||
flt(args.get("total_number_of_depreciations"), 2)
|
||||
- flt(self.opening_number_of_booked_depreciations)
|
||||
)
|
||||
* flt(args.get("frequency_of_depreciation"))
|
||||
)
|
||||
/ 12
|
||||
|
||||
@@ -144,6 +144,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
|
||||
"has_certificate": task.certificate_required,
|
||||
"description": task.description,
|
||||
"assign_to_name": task.assign_to_name,
|
||||
"task_assignee_email": task.assign_to,
|
||||
"periodicity": str(task.periodicity),
|
||||
"maintenance_type": task.maintenance_type,
|
||||
"due_date": task.next_due_date,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"column_break_6",
|
||||
"maintenance_status",
|
||||
"assign_to_name",
|
||||
"task_assignee_email",
|
||||
"due_date",
|
||||
"completion_date",
|
||||
"description",
|
||||
@@ -168,15 +169,22 @@
|
||||
"in_preview": 1,
|
||||
"label": "Task Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "task_assignee_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Task Assignee Email",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-22 12:33:45.888124",
|
||||
"modified": "2024-09-24 15:12:37.497853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Log",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -199,4 +207,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class AssetMaintenanceLog(Document):
|
||||
naming_series: DF.Literal["ACC-AML-.YYYY.-"]
|
||||
periodicity: DF.Data | None
|
||||
task: DF.Link | None
|
||||
task_assignee_email: DF.Data | None
|
||||
task_name: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
if (doc.status != "Closed") {
|
||||
if (doc.status != "On Hold") {
|
||||
if (flt(doc.per_received, 2) < 100 && allow_receipt) {
|
||||
if (flt(doc.per_received) < 100 && allow_receipt) {
|
||||
cur_frm.add_custom_button(
|
||||
__("Purchase Receipt"),
|
||||
this.make_purchase_receipt,
|
||||
@@ -408,7 +408,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flt(doc.per_billed, 2) < 100)
|
||||
if (flt(doc.per_billed) < 100)
|
||||
cur_frm.add_custom_button(
|
||||
__("Purchase Invoice"),
|
||||
this.make_purchase_invoice,
|
||||
|
||||
@@ -738,7 +738,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
@@ -819,7 +819,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
}
|
||||
|
||||
doc = get_mapped_doc(
|
||||
@@ -889,6 +889,20 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.populate_items_table()
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
else:
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = json.loads(target_doc)
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
@@ -919,22 +933,9 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_process,
|
||||
)
|
||||
|
||||
target_doc.populate_items_table()
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
else:
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
|
||||
@@ -14,18 +14,25 @@ def get_data():
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Supplier Quotation": ["items", "supplier_quotation"],
|
||||
"Project": ["items", "project"],
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"BOM": ["items", "bom"],
|
||||
"Production Plan": ["items", "production_plan"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]},
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]},
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
||||
"items": ["Supplier Quotation", "Project", "Auto Repeat"],
|
||||
},
|
||||
{
|
||||
"label": _("Manufacturing"),
|
||||
"items": ["Material Request", "BOM", "Production Plan", "Blanket Order"],
|
||||
},
|
||||
{
|
||||
"label": _("Sub-contracting"),
|
||||
"items": ["Subcontracting Order", "Subcontracting Receipt", "Stock Entry"],
|
||||
},
|
||||
{"label": _("Internal"), "items": ["Sales Order"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ frappe.listview_settings["Purchase Order"] = {
|
||||
"status",
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
// Please do not add precision in the flt function
|
||||
if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (doc.status === "On Hold") {
|
||||
return [__("On Hold"), "orange", "status,=,On Hold"];
|
||||
} else if (doc.status === "Delivered") {
|
||||
return [__("Delivered"), "green", "status,=,Closed"];
|
||||
} else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") {
|
||||
if (flt(doc.per_billed, 2) < 100) {
|
||||
} else if (flt(doc.per_received) < 100 && doc.status !== "Closed") {
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
return [
|
||||
__("To Receive and Bill"),
|
||||
"orange",
|
||||
@@ -26,17 +27,9 @@ frappe.listview_settings["Purchase Order"] = {
|
||||
} else {
|
||||
return [__("To Receive"), "orange", "per_received,<,100|per_billed,=,100|status,!=,Closed"];
|
||||
}
|
||||
} else if (
|
||||
flt(doc.per_received, 2) >= 100 &&
|
||||
flt(doc.per_billed, 2) < 100 &&
|
||||
doc.status !== "Closed"
|
||||
) {
|
||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) < 100 && doc.status !== "Closed") {
|
||||
return [__("To Bill"), "orange", "per_received,=,100|per_billed,<,100|status,!=,Closed"];
|
||||
} else if (
|
||||
flt(doc.per_received, 2) >= 100 &&
|
||||
flt(doc.per_billed, 2) == 100 &&
|
||||
doc.status !== "Closed"
|
||||
) {
|
||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) == 100 && doc.status !== "Closed") {
|
||||
return [__("Completed"), "green", "per_received,=,100|per_billed,=,100|status,!=,Closed"];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,8 +146,8 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
return;
|
||||
}
|
||||
},
|
||||
"Download PDF for Supplier",
|
||||
"Download"
|
||||
__("Download PDF for Supplier"),
|
||||
__("Download")
|
||||
);
|
||||
},
|
||||
__("Tools")
|
||||
@@ -272,9 +272,10 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
});
|
||||
};
|
||||
|
||||
dialog.fields_dict.note.$wrapper
|
||||
.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
|
||||
automatically be attached with the email.</p>`);
|
||||
const msg = __(
|
||||
"This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email."
|
||||
);
|
||||
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">${msg}</p>`);
|
||||
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
@@ -390,6 +390,7 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
|
||||
"Request for Quotation": {
|
||||
"doctype": "Supplier Quotation",
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
"field_map": {"opportunity": "opportunity"},
|
||||
},
|
||||
"Request for Quotation Item": {
|
||||
"doctype": "Supplier Quotation Item",
|
||||
@@ -455,6 +456,7 @@ def create_rfq_items(sq_doc, supplier, data):
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"stock_qty",
|
||||
"uom",
|
||||
]:
|
||||
args[field] = data.get(field)
|
||||
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
frappe.query_reports["Purchase Order Trends"]["filters"].push({
|
||||
fieldname: "include_closed_orders",
|
||||
label: __("Include Closed Orders"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
});
|
||||
|
||||
@@ -345,9 +345,21 @@ class AccountsController(TransactionBase):
|
||||
repost_doc.flags.ignore_links = True
|
||||
repost_doc.save(ignore_permissions=True)
|
||||
|
||||
def _remove_advance_payment_ledger_entries(self):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
|
||||
def on_trash(self):
|
||||
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
|
||||
|
||||
self._remove_advance_payment_ledger_entries()
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
@@ -393,12 +405,15 @@ class AccountsController(TransactionBase):
|
||||
def validate_return_against_account(self):
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
|
||||
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
|
||||
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
|
||||
cr_dr_account = self.get(cr_dr_account_field)
|
||||
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
|
||||
original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field)
|
||||
if original_account != self.get(cr_dr_account_field):
|
||||
frappe.throw(
|
||||
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
|
||||
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
|
||||
_(
|
||||
"Please set {0} to {1}, the same account that was used in the original invoice {2}."
|
||||
).format(
|
||||
frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)),
|
||||
frappe.bold(original_account),
|
||||
frappe.bold(self.return_against),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -448,6 +463,11 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
self.set_due_date()
|
||||
self.set_payment_schedule()
|
||||
@@ -462,7 +482,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_payment_schedule_amount()
|
||||
|
||||
def validate_all_documents_schedule(self):
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
self.validate_invoice_documents_schedule()
|
||||
elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
|
||||
self.validate_non_invoice_documents_schedule()
|
||||
@@ -1063,6 +1083,13 @@ class AccountsController(TransactionBase):
|
||||
"Stock Entry": "stock_entry_type",
|
||||
"Asset Capitalization": "entry_type",
|
||||
}
|
||||
|
||||
for method_name in frappe.get_hooks("voucher_subtypes"):
|
||||
voucher_subtype = frappe.get_attr(method_name)(self)
|
||||
|
||||
if voucher_subtype:
|
||||
return voucher_subtype
|
||||
|
||||
if self.doctype in voucher_subtypes:
|
||||
return self.get(voucher_subtypes[self.doctype])
|
||||
elif self.doctype == "Purchase Receipt" and self.is_return:
|
||||
@@ -1073,6 +1100,7 @@ class AccountsController(TransactionBase):
|
||||
return "Credit Note"
|
||||
elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
|
||||
return "Debit Note"
|
||||
|
||||
return self.doctype
|
||||
|
||||
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
|
||||
@@ -1919,21 +1947,23 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return stock_items
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
party = self.customer if self.doctype == "Sales Order" else self.supplier
|
||||
def calculate_total_advance_from_ledger(self):
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
advance = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
|
||||
frappe.qb.from_(adv)
|
||||
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount"))
|
||||
.where(
|
||||
(ple.against_voucher_type == self.doctype)
|
||||
& (ple.against_voucher_no == self.name)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
& (ple.company == self.company)
|
||||
(adv.against_voucher_type == self.doctype)
|
||||
& (adv.against_voucher_no == self.name)
|
||||
& (adv.company == self.company)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return advance
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
advance = self.calculate_total_advance_from_ledger()
|
||||
advance_paid, order_total = None, None
|
||||
|
||||
if advance:
|
||||
advance = advance[0]
|
||||
@@ -1966,7 +1996,7 @@ class AccountsController(TransactionBase):
|
||||
).format(formatted_advance_paid, self.name, formatted_order_total)
|
||||
)
|
||||
|
||||
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
|
||||
@property
|
||||
def company_abbr(self):
|
||||
@@ -2530,6 +2560,67 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.accounts if x.reference_type in advance_payment_doctypes
|
||||
]
|
||||
|
||||
for x in advance_doctype_references:
|
||||
# Looking for payments
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if x.account_type == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
amount = x.get(dr_or_cr)
|
||||
if amount > 0:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_type
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = amount if self.docstatus == 1 else -1 * amount
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.currency = x.account_currency
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_for_payment(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.references if x.reference_doctype in advance_payment_doctypes
|
||||
]
|
||||
currency = (
|
||||
self.paid_from_account_currency
|
||||
if self.payment_type == "Receive"
|
||||
else self.paid_to_account_currency
|
||||
)
|
||||
for x in advance_doctype_references:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_doctype
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount
|
||||
doc.currency = currency
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_entries(self):
|
||||
if self.docstatus != 0:
|
||||
if self.doctype == "Journal Entry":
|
||||
self.make_advance_payment_ledger_for_journal()
|
||||
elif self.doctype == "Payment Entry":
|
||||
self.make_advance_payment_ledger_for_payment()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@@ -3309,7 +3400,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
@@ -3425,25 +3515,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
@@ -3518,6 +3604,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
# Cancel and Recreate Stock Reservation Entries.
|
||||
if parent_doctype == "Sales Order":
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
|
||||
@@ -702,9 +702,11 @@ class BuyingController(SubcontractingController):
|
||||
if self.get("is_return"):
|
||||
return
|
||||
|
||||
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
|
||||
"Buying Settings", "disable_last_purchase_rate"
|
||||
):
|
||||
if self.doctype in [
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
update_last_purchase_rate(self, is_submit=0)
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
|
||||
@@ -79,6 +79,9 @@ def validate_returned_items(doc):
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
select_fields += ",rejected_qty, received_qty"
|
||||
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
select_fields += ",name"
|
||||
|
||||
for d in frappe.db.sql(
|
||||
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
|
||||
doc.return_against,
|
||||
@@ -104,15 +107,24 @@ def validate_returned_items(doc):
|
||||
|
||||
items_returned = False
|
||||
for d in doc.get("items"):
|
||||
key = d.item_code
|
||||
raise_exception = False
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
field = frappe.scrub(doc.doctype) + "_item"
|
||||
if d.get(field):
|
||||
key = (d.item_code, d.get(field))
|
||||
raise_exception = True
|
||||
|
||||
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
|
||||
if d.item_code not in valid_items:
|
||||
frappe.throw(
|
||||
if key not in valid_items:
|
||||
frappe.msgprint(
|
||||
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
|
||||
d.idx, d.item_code, doc.doctype, doc.return_against
|
||||
)
|
||||
),
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
else:
|
||||
ref = valid_items.get(d.item_code, frappe._dict())
|
||||
ref = valid_items.get(key, frappe._dict())
|
||||
validate_quantity(doc, d, ref, valid_items, already_returned_items)
|
||||
|
||||
if (
|
||||
@@ -193,8 +205,12 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
def get_ref_item_dict(valid_items, ref_item_row):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
key = ref_item_row.item_code
|
||||
if ref_item_row.get("name"):
|
||||
key = (ref_item_row.item_code, ref_item_row.name)
|
||||
|
||||
valid_items.setdefault(
|
||||
ref_item_row.item_code,
|
||||
key,
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": 0,
|
||||
@@ -208,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
|
||||
}
|
||||
),
|
||||
)
|
||||
item_dict = valid_items[ref_item_row.item_code]
|
||||
item_dict = valid_items[key]
|
||||
item_dict["qty"] += ref_item_row.qty
|
||||
item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0)
|
||||
if ref_item_row.get("rate", 0) > item_dict["rate"]:
|
||||
@@ -335,6 +351,9 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
|
||||
if source.tax_withholding_category:
|
||||
doc.set_onload("supplier_tds", source.tax_withholding_category)
|
||||
elif doctype == "Delivery Note":
|
||||
# manual additions to the return should hit the return warehous, too
|
||||
doc.set_warehouse = default_warehouse_for_sales_return
|
||||
|
||||
for tax in doc.get("taxes") or []:
|
||||
if tax.charge_type == "Actual":
|
||||
@@ -581,6 +600,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle:
|
||||
target_doc.serial_no = None
|
||||
target_doc.batch_no = None
|
||||
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
@@ -883,6 +906,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Entry`.`incoming_rate`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
@@ -904,15 +928,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
{
|
||||
"qty": 0.0,
|
||||
"serial_nos": defaultdict(float),
|
||||
"batches": defaultdict(float),
|
||||
"serial_nos_valuation": defaultdict(float),
|
||||
"batches_valuation": defaultdict(float),
|
||||
}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate
|
||||
|
||||
return available_dict
|
||||
|
||||
@@ -948,12 +980,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
fields = ["serial_and_batch_bundle"]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
fields.append("rejected_serial_and_batch_bundle")
|
||||
|
||||
if doctype == "Purchase Receipt Item":
|
||||
fields.append("return_qty_from_rejected_warehouse")
|
||||
|
||||
del filters["rejected_serial_and_batch_bundle"]
|
||||
data = frappe.get_all(
|
||||
@@ -987,7 +1020,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
filterd_serial_batch = frappe._dict(
|
||||
{
|
||||
"serial_nos": [],
|
||||
"batches": defaultdict(float),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
}
|
||||
)
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
@@ -997,7 +1037,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
@@ -1082,6 +1122,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
|
||||
@@ -473,6 +473,16 @@ class SellingController(StockController):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
and self.get("return_against")
|
||||
and self.get("is_return")
|
||||
and get_valuation_method(d.item_code) == "Moving Average"
|
||||
):
|
||||
d.incoming_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Delivery Note" or self.get("update_stock"):
|
||||
|
||||
@@ -63,6 +63,33 @@ class StockController(AccountsController):
|
||||
self.set_rate_of_stock_uom()
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
for row in self.get("items"):
|
||||
if row.uom != row.stock_uom:
|
||||
continue
|
||||
|
||||
if row.conversion_factor != 1.0:
|
||||
row.conversion_factor = 1.0
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}."
|
||||
).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def validate_items_exist(self):
|
||||
if not self.get("items"):
|
||||
return
|
||||
|
||||
items = [d.item_code for d in self.get("items")]
|
||||
|
||||
exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name")
|
||||
non_exists_items = set(items) - set(exists_items)
|
||||
|
||||
if non_exists_items:
|
||||
frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items)))
|
||||
|
||||
def validate_duplicate_serial_and_batch_bundle(self, table_name):
|
||||
if not self.get(table_name):
|
||||
@@ -307,6 +334,11 @@ class StockController(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||
row.db_set(
|
||||
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
@@ -345,6 +377,9 @@ class StockController(AccountsController):
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
frappe.throw(_("Item {0} does not exist.").format(bold(item_code)))
|
||||
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
|
||||
@@ -561,11 +561,11 @@ class SubcontractingController(StockController):
|
||||
use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = qty
|
||||
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
|
||||
rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty"))
|
||||
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
|
||||
else:
|
||||
rm_obj.consumed_qty = qty
|
||||
rm_obj.required_qty = bom_item.required_qty or qty
|
||||
rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty"))
|
||||
rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty"))
|
||||
rm_obj.serial_and_batch_bundle = None
|
||||
setattr(
|
||||
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
|
||||
@@ -576,30 +576,56 @@ class SubcontractingController(StockController):
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": rm_obj.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": -1 * flt(rm_obj.consumed_qty),
|
||||
"actual_qty": -1 * flt(rm_obj.consumed_qty),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": item_row.name,
|
||||
"company": self.company,
|
||||
"allow_zero_valuation": 1,
|
||||
}
|
||||
)
|
||||
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
|
||||
if rm_obj.serial_and_batch_bundle:
|
||||
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
return
|
||||
|
||||
for row in self.supplied_items:
|
||||
item_row = None
|
||||
if row.reference_name:
|
||||
item_row = self.get_item_row(row.reference_name)
|
||||
|
||||
if not item_row:
|
||||
continue
|
||||
|
||||
self.set_rate_for_supplied_items(row, item_row)
|
||||
|
||||
def get_item_row(self, reference_name):
|
||||
for item in self.items:
|
||||
if item.name == reference_name:
|
||||
return item
|
||||
|
||||
def set_rate_for_supplied_items(self, rm_obj, item_row):
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": rm_obj.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": -1 * flt(rm_obj.consumed_qty),
|
||||
"actual_qty": -1 * flt(rm_obj.consumed_qty),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": item_row.name,
|
||||
"company": self.company,
|
||||
"allow_zero_valuation": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if rm_obj.serial_and_batch_bundle:
|
||||
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
|
||||
|
||||
if rm_obj.use_serial_batch_fields:
|
||||
args["batch_no"] = rm_obj.batch_no
|
||||
args["serial_no"] = rm_obj.serial_no
|
||||
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
@@ -638,8 +664,8 @@ class SubcontractingController(StockController):
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
|
||||
rm_obj.required_qty = required_qty
|
||||
rm_obj.consumed_qty = consumed_qty
|
||||
rm_obj.required_qty = flt(required_qty, rm_obj.precision("required_qty"))
|
||||
rm_obj.consumed_qty = flt(consumed_qty, rm_obj.precision("consumed_qty"))
|
||||
|
||||
def __set_serial_nos(self, item_row, rm_obj):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
@@ -1209,6 +1235,17 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
|
||||
def make_return_stock_entry_for_subcontract(
|
||||
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
|
||||
):
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.purpose = "Material Transfer"
|
||||
|
||||
if source_doc.doctype == "Purchase Order":
|
||||
target_doc.purchase_order = source_doc.name
|
||||
else:
|
||||
target_doc.subcontracting_order = source_doc.name
|
||||
|
||||
target_doc.company = source_doc.company
|
||||
target_doc.is_return = 1
|
||||
|
||||
ste_doc = get_mapped_doc(
|
||||
order_doctype,
|
||||
order_doc.name,
|
||||
@@ -1219,18 +1256,13 @@ def make_return_stock_entry_for_subcontract(
|
||||
},
|
||||
},
|
||||
ignore_child_tables=True,
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
ste_doc.purpose = "Material Transfer"
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
ste_doc.purchase_order = order_doc.name
|
||||
rm_detail_field = "po_detail"
|
||||
else:
|
||||
ste_doc.subcontracting_order = order_doc.name
|
||||
rm_detail_field = "sco_rm_detail"
|
||||
ste_doc.company = order_doc.company
|
||||
ste_doc.is_return = 1
|
||||
|
||||
for _key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
|
||||
@@ -40,7 +40,7 @@ class calculate_taxes_and_totals:
|
||||
return items
|
||||
|
||||
def calculate(self):
|
||||
if not len(self._items):
|
||||
if not len(self.doc.items):
|
||||
return
|
||||
|
||||
self.discount_amount_applied = False
|
||||
@@ -95,7 +95,7 @@ class calculate_taxes_and_totals:
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
args = {
|
||||
@@ -154,7 +154,7 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
@@ -258,7 +258,7 @@ class calculate_taxes_and_totals:
|
||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
for item in self.doc.items:
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
cumulated_tax_fraction = 0
|
||||
total_inclusive_tax_amount_per_qty = 0
|
||||
|
||||
@@ -1234,6 +1234,7 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA6": {},
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
"Subcontracted Item SA9": {"stock_uom": "Litre"},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
@@ -1254,6 +1255,7 @@ def make_raw_materials():
|
||||
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
|
||||
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
|
||||
"Subcontracted SRM Item 8": {},
|
||||
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
|
||||
}
|
||||
|
||||
for item, properties in raw_materials.items():
|
||||
@@ -1280,6 +1282,7 @@ def make_service_items():
|
||||
"Subcontracted Service Item 6": {},
|
||||
"Subcontracted Service Item 7": {},
|
||||
"Subcontracted Service Item 8": {},
|
||||
"Subcontracted Service Item 9": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
|
||||
@@ -69,13 +69,15 @@ def get_data(filters, conditions):
|
||||
"Delivery Note",
|
||||
]:
|
||||
posting_date = "t1.posting_date"
|
||||
if filters.period_based_on:
|
||||
if filters.period_based_on and conditions.get("trans") in ["Sales Invoice", "Purchase Invoice"]:
|
||||
posting_date = "t1." + filters.period_based_on
|
||||
|
||||
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
||||
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||
cond += " and t1.status != 'Closed'"
|
||||
|
||||
if not filters.get("include_closed_orders"):
|
||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||
cond += " and t1.status != 'Closed'"
|
||||
|
||||
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
||||
cond += " and t1.quotation_to = 'Customer'"
|
||||
@@ -222,7 +224,7 @@ def period_wise_columns_query(filters, trans):
|
||||
|
||||
if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]:
|
||||
trans_date = "posting_date"
|
||||
if filters.period_based_on:
|
||||
if filters.period_based_on and trans in ["Purchase Invoice", "Sales Invoice"]:
|
||||
trans_date = filters.period_based_on
|
||||
else:
|
||||
trans_date = "transaction_date"
|
||||
|
||||
@@ -7,9 +7,9 @@ cur_frm.email_field = "email_id";
|
||||
erpnext.LeadController = class LeadController extends frappe.ui.form.Controller {
|
||||
setup() {
|
||||
this.frm.make_methods = {
|
||||
Customer: this.make_customer,
|
||||
Quotation: this.make_quotation,
|
||||
Opportunity: this.make_opportunity,
|
||||
Customer: this.make_customer.bind(this),
|
||||
Quotation: this.make_quotation.bind(this),
|
||||
Opportunity: this.make_opportunity.bind(this),
|
||||
};
|
||||
|
||||
// For avoiding integration issues.
|
||||
@@ -28,18 +28,18 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
erpnext.toggle_naming_series();
|
||||
|
||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Opportunity"),
|
||||
function () {
|
||||
me.frm.trigger("make_opportunity");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
this.frm.add_custom_button(__("Customer"), this.make_customer.bind(this), __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity.bind(this), __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
|
||||
if (!doc.__onload.linked_prospects.length) {
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__("Add to Prospect"), this.add_lead_to_prospect, __("Action"));
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect.bind(this), __("Create"));
|
||||
this.frm.add_custom_button(
|
||||
__("Add to Prospect"),
|
||||
() => {
|
||||
this.add_lead_to_prospect(this.frm);
|
||||
},
|
||||
__("Action")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
add_lead_to_prospect() {
|
||||
let me = this;
|
||||
add_lead_to_prospect(frm) {
|
||||
frappe.prompt(
|
||||
[
|
||||
{
|
||||
@@ -69,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
frappe.call({
|
||||
method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect",
|
||||
args: {
|
||||
lead: me.frm.doc.name,
|
||||
lead: frm.doc.name,
|
||||
prospect: data.prospect,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.reload_doc();
|
||||
frm.reload_doc();
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
@@ -89,32 +88,123 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
make_customer() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_customer",
|
||||
frm: cur_frm,
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_quotation() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||
frm: cur_frm,
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
async make_opportunity() {
|
||||
const frm = this.frm;
|
||||
let existing_prospect = (
|
||||
await frappe.db.get_value(
|
||||
"Prospect Lead",
|
||||
{
|
||||
lead: frm.doc.name,
|
||||
},
|
||||
"name",
|
||||
null,
|
||||
"Prospect"
|
||||
)
|
||||
).message?.name;
|
||||
|
||||
let fields = [];
|
||||
if (!existing_prospect) {
|
||||
fields.push(
|
||||
{
|
||||
label: "Create Prospect",
|
||||
fieldname: "create_prospect",
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
label: "Prospect Name",
|
||||
fieldname: "prospect_name",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.company_name,
|
||||
depends_on: "create_prospect",
|
||||
mandatory_depends_on: "create_prospect",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await frm.reload_doc();
|
||||
|
||||
let existing_contact = (
|
||||
await frappe.db.get_value(
|
||||
"Contact",
|
||||
{
|
||||
first_name: frm.doc.first_name || frm.doc.lead_name,
|
||||
last_name: frm.doc.last_name,
|
||||
},
|
||||
"name"
|
||||
)
|
||||
).message?.name;
|
||||
|
||||
if (!existing_contact) {
|
||||
fields.push({
|
||||
label: "Create Contact",
|
||||
fieldname: "create_contact",
|
||||
fieldtype: "Check",
|
||||
default: "1",
|
||||
});
|
||||
}
|
||||
|
||||
if (fields.length) {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Create Opportunity"),
|
||||
fields: fields,
|
||||
primary_action: function (data) {
|
||||
frappe.call({
|
||||
method: "create_prospect_and_contact",
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Create"),
|
||||
});
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
make_prospect() {
|
||||
const me = this;
|
||||
frappe.model.with_doctype("Prospect", function () {
|
||||
let prospect = frappe.model.get_new_doc("Prospect");
|
||||
prospect.company_name = cur_frm.doc.company_name;
|
||||
prospect.no_of_employees = cur_frm.doc.no_of_employees;
|
||||
prospect.industry = cur_frm.doc.industry;
|
||||
prospect.market_segment = cur_frm.doc.market_segment;
|
||||
prospect.territory = cur_frm.doc.territory;
|
||||
prospect.fax = cur_frm.doc.fax;
|
||||
prospect.website = cur_frm.doc.website;
|
||||
prospect.prospect_owner = cur_frm.doc.lead_owner;
|
||||
prospect.notes = cur_frm.doc.notes;
|
||||
prospect.company_name = me.frm.doc.company_name;
|
||||
prospect.no_of_employees = me.frm.doc.no_of_employees;
|
||||
prospect.industry = me.frm.doc.industry;
|
||||
prospect.market_segment = me.frm.doc.market_segment;
|
||||
prospect.territory = me.frm.doc.territory;
|
||||
prospect.fax = me.frm.doc.fax;
|
||||
prospect.website = me.frm.doc.website;
|
||||
prospect.prospect_owner = me.frm.doc.lead_owner;
|
||||
prospect.notes = me.frm.doc.notes;
|
||||
|
||||
let leads_row = frappe.model.add_child(prospect, "leads");
|
||||
leads_row.lead = cur_frm.doc.name;
|
||||
leads_row.lead = me.frm.doc.name;
|
||||
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
});
|
||||
@@ -150,90 +240,3 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||
|
||||
frappe.ui.form.on("Lead", {
|
||||
make_opportunity: async function (frm) {
|
||||
let existing_prospect = (
|
||||
await frappe.db.get_value(
|
||||
"Prospect Lead",
|
||||
{
|
||||
lead: frm.doc.name,
|
||||
},
|
||||
"name",
|
||||
null,
|
||||
"Prospect"
|
||||
)
|
||||
).message.name;
|
||||
|
||||
if (!existing_prospect) {
|
||||
var fields = [
|
||||
{
|
||||
label: "Create Prospect",
|
||||
fieldname: "create_prospect",
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
label: "Prospect Name",
|
||||
fieldname: "prospect_name",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.company_name,
|
||||
depends_on: "create_prospect",
|
||||
},
|
||||
];
|
||||
}
|
||||
let existing_contact = (
|
||||
await frappe.db.get_value(
|
||||
"Contact",
|
||||
{
|
||||
first_name: frm.doc.first_name || frm.doc.lead_name,
|
||||
last_name: frm.doc.last_name,
|
||||
},
|
||||
"name"
|
||||
)
|
||||
).message.name;
|
||||
|
||||
if (!existing_contact) {
|
||||
fields.push({
|
||||
label: "Create Contact",
|
||||
fieldname: "create_contact",
|
||||
fieldtype: "Check",
|
||||
default: "1",
|
||||
});
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Create Opportunity"),
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: "create_prospect_and_contact",
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Create"),
|
||||
});
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -365,7 +365,6 @@ doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||
],
|
||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
|
||||
@@ -968,6 +968,13 @@ class BOM(WebsiteGenerator):
|
||||
if not d.batch_size or d.batch_size <= 0:
|
||||
d.batch_size = 1
|
||||
|
||||
if not d.workstation and not d.workstation_type:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
|
||||
).format(d.idx, d.operation)
|
||||
)
|
||||
|
||||
def get_tree_representation(self) -> BOMTree:
|
||||
"""Get a complete tree representation preserving order of child items."""
|
||||
return BOMTree(self.name)
|
||||
|
||||
@@ -669,7 +669,7 @@ class JobCard(Document):
|
||||
self.set_transferred_qty()
|
||||
|
||||
def validate_transfer_qty(self):
|
||||
if self.items and self.transferred_qty < self.for_quantity:
|
||||
if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
||||
@@ -941,26 +941,19 @@ class JobCard(Document):
|
||||
|
||||
qty = 0
|
||||
if self.work_order:
|
||||
doc = frappe.get_doc("Work Order", self.work_order)
|
||||
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
||||
completed = True
|
||||
min_qty = []
|
||||
for d in doc.operations:
|
||||
if d.status != "Completed":
|
||||
completed = False
|
||||
if d.completed_qty:
|
||||
min_qty.append(d.completed_qty)
|
||||
else:
|
||||
min_qty = []
|
||||
break
|
||||
|
||||
if completed:
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
|
||||
fields="sum(transferred_qty) as qty",
|
||||
group_by="operation_id",
|
||||
)
|
||||
if min_qty:
|
||||
qty = min(min_qty)
|
||||
|
||||
if job_cards:
|
||||
qty = min(d.qty for d in job_cards)
|
||||
|
||||
doc.db_set("material_transferred_for_manufacturing", qty)
|
||||
doc.db_set("material_transferred_for_manufacturing", qty)
|
||||
|
||||
self.set_status(update_status)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user