mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-30 15:54:35 +00:00
Compare commits
445 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f47b626c | ||
|
|
0283f7526c | ||
|
|
3ad451dd6e | ||
|
|
9e409bde2e | ||
|
|
8459166323 | ||
|
|
e1328de712 | ||
|
|
d3a2350b3e | ||
|
|
739cd18604 | ||
|
|
e61cc9b12e | ||
|
|
baa564fc94 | ||
|
|
a55ec56fbf | ||
|
|
3f9df2fb2d | ||
|
|
7f147446df | ||
|
|
8def42f751 | ||
|
|
79e6550321 | ||
|
|
cc30a01898 | ||
|
|
877d5bd3aa | ||
|
|
0b53bd3e9a | ||
|
|
3606fe8fba | ||
|
|
0b921016ff | ||
|
|
4d49608a68 | ||
|
|
bac36f342d | ||
|
|
a4b8b4c771 | ||
|
|
3cef94e2ed | ||
|
|
13711993fe | ||
|
|
24678b0e24 | ||
|
|
5edbd8851a | ||
|
|
6582e4c5f9 | ||
|
|
fd1c1ba35e | ||
|
|
db6ae61935 | ||
|
|
73f11cf19e | ||
|
|
8f7dc827ea | ||
|
|
0d044bc5bb | ||
|
|
14ee2d239a | ||
|
|
fc8eeaf4f6 | ||
|
|
aad8c88532 | ||
|
|
e783536ba0 | ||
|
|
ee3feba386 | ||
|
|
cb9be11448 | ||
|
|
31dc6021e2 | ||
|
|
229f4d3d92 | ||
|
|
c25862a85f | ||
|
|
f96848a3b9 | ||
|
|
ac97489a32 | ||
|
|
02ff406b7c | ||
|
|
428aedc29c | ||
|
|
6af24dca6e | ||
|
|
8980eb9b9d | ||
|
|
4dfdb2b0a1 | ||
|
|
9554a49bbd | ||
|
|
5b69445294 | ||
|
|
9c703765a1 | ||
|
|
043539fcdb | ||
|
|
54a76d8932 | ||
|
|
b6106212c1 | ||
|
|
c2001e9c67 | ||
|
|
47429095a2 | ||
|
|
2f3f87fe7e | ||
|
|
2b4dfca3ff | ||
|
|
a18721d21c | ||
|
|
34f03d608a | ||
|
|
b786cd30e6 | ||
|
|
985fb5dfdc | ||
|
|
8264d42cd9 | ||
|
|
8b67527900 | ||
|
|
2ce8299bc8 | ||
|
|
84b03485d6 | ||
|
|
d0b14f1907 | ||
|
|
cd1803a74d | ||
|
|
a1cf27ec17 | ||
|
|
61880a311a | ||
|
|
2d290b153d | ||
|
|
cacb720556 | ||
|
|
3bdd4ce116 | ||
|
|
c479998cd6 | ||
|
|
501e388186 | ||
|
|
af45ec0d6d | ||
|
|
3015628519 | ||
|
|
8b6eea6349 | ||
|
|
81c29e8f8c | ||
|
|
04758d3de3 | ||
|
|
615b0c40a3 | ||
|
|
231abab321 | ||
|
|
fff3b1e84e | ||
|
|
5299a1032b | ||
|
|
1e5fbc0a48 | ||
|
|
5c47c35a0f | ||
|
|
2bf910a786 | ||
|
|
0b8673777a | ||
|
|
8f4c1e7169 | ||
|
|
f303245fae | ||
|
|
cd21e5c652 | ||
|
|
57e0f73595 | ||
|
|
2c73e31742 | ||
|
|
002685fc89 | ||
|
|
38a3a43ba5 | ||
|
|
07f938cc10 | ||
|
|
5c013172f9 | ||
|
|
b76c96820e | ||
|
|
1d56931050 | ||
|
|
66dc79ceb5 | ||
|
|
00cbc89b5f | ||
|
|
d7baa451e3 | ||
|
|
e0b5386bf0 | ||
|
|
133dca1824 | ||
|
|
eb0df50f1b | ||
|
|
5761cfb3d5 | ||
|
|
568b582b6a | ||
|
|
d115c1c156 | ||
|
|
4e688778dc | ||
|
|
b5890c1d55 | ||
|
|
6419d020a1 | ||
|
|
60364f6dc9 | ||
|
|
23c4252b9b | ||
|
|
c816f9bd0a | ||
|
|
de46165768 | ||
|
|
5cc251a172 | ||
|
|
7c8b34fd8f | ||
|
|
83320c97fa | ||
|
|
b004865240 | ||
|
|
525780645a | ||
|
|
12bf31df87 | ||
|
|
041335f318 | ||
|
|
9c9c8c9356 | ||
|
|
8e65b0ec0c | ||
|
|
bb553c27ab | ||
|
|
7080e1422d | ||
|
|
7047fe2681 | ||
|
|
f247f02e49 | ||
|
|
7bc7557018 | ||
|
|
e72264f448 | ||
|
|
1dcbdf3257 | ||
|
|
5d6730a059 | ||
|
|
6b1d20970e | ||
|
|
528107e224 | ||
|
|
86b917b04c | ||
|
|
9875489758 | ||
|
|
939cf321f7 | ||
|
|
29f3aac925 | ||
|
|
1a382ebe86 | ||
|
|
6c1ceff8ee | ||
|
|
1e85f69072 | ||
|
|
1992c2639c | ||
|
|
8f278ab7c7 | ||
|
|
7a33bf41d8 | ||
|
|
6df9cf327d | ||
|
|
5e06e4acce | ||
|
|
7749814571 | ||
|
|
6f760d197d | ||
|
|
5668795884 | ||
|
|
aaf35c5df9 | ||
|
|
6aa8803068 | ||
|
|
506dd3c6b9 | ||
|
|
9a433a6750 | ||
|
|
e3ce17bd6e | ||
|
|
836fd8fbc4 | ||
|
|
fe8c9a3605 | ||
|
|
941d67a0b6 | ||
|
|
5a3073c4c1 | ||
|
|
8f2fdcae88 | ||
|
|
a09c57f0d1 | ||
|
|
7b13d8cd98 | ||
|
|
ee41e55343 | ||
|
|
fdaf5fafda | ||
|
|
c85fd36960 | ||
|
|
a4e5a46566 | ||
|
|
3c39888227 | ||
|
|
d5f07f06c7 | ||
|
|
ccc0358db6 | ||
|
|
1ff085876e | ||
|
|
1c6e4649bd | ||
|
|
e4e1be568b | ||
|
|
01db730714 | ||
|
|
dbcffa7ea4 | ||
|
|
36fa6bf15c | ||
|
|
4f80ddd834 | ||
|
|
400f4f32ad | ||
|
|
363129bcd4 | ||
|
|
087dde5873 | ||
|
|
5ae9faab91 | ||
|
|
446a8fe096 | ||
|
|
0b50f1a9c3 | ||
|
|
f29c43811c | ||
|
|
e4fbd22173 | ||
|
|
6b56724436 | ||
|
|
af49f5a8af | ||
|
|
46b0734d6f | ||
|
|
d41303961c | ||
|
|
c5717b983d | ||
|
|
d02d005913 | ||
|
|
d10add4b1e | ||
|
|
88234bbf9a | ||
|
|
d8a1d0e908 | ||
|
|
7437cea458 | ||
|
|
1790bcc6d1 | ||
|
|
d0b8e0da8d | ||
|
|
ab4b1d4356 | ||
|
|
7759775ee6 | ||
|
|
1b00de1815 | ||
|
|
41ab7f3f7c | ||
|
|
5049f80b7f | ||
|
|
d650b23722 | ||
|
|
75bc68b863 | ||
|
|
f37fe1781a | ||
|
|
8bd71954f3 | ||
|
|
0f86ed28bc | ||
|
|
ddcf79da1d | ||
|
|
b5fcd682a6 | ||
|
|
29405498bd | ||
|
|
1630979f05 | ||
|
|
6ab7d98681 | ||
|
|
eee500f20e | ||
|
|
78a329e573 | ||
|
|
83dcbec86a | ||
|
|
5b6ed1d077 | ||
|
|
284c93ecc2 | ||
|
|
33051ec17e | ||
|
|
9e54e2ea58 | ||
|
|
c09c4cc243 | ||
|
|
489efda985 | ||
|
|
3a03865a8f | ||
|
|
854632dd51 | ||
|
|
ef44ad79a0 | ||
|
|
a8b31df65d | ||
|
|
9f4311e7fb | ||
|
|
0a65217423 | ||
|
|
ef195513d0 | ||
|
|
f3cafef6a7 | ||
|
|
6762dc3392 | ||
|
|
dbd47dff98 | ||
|
|
8ed512f6c6 | ||
|
|
3a6ef2564e | ||
|
|
35df539da3 | ||
|
|
5e083861a4 | ||
|
|
41e9a10ab4 | ||
|
|
90dea426d8 | ||
|
|
bd48d391e4 | ||
|
|
9082ff6aa9 | ||
|
|
ba0ce267c2 | ||
|
|
cc535b7636 | ||
|
|
81c7b8c273 | ||
|
|
e94f0b1cca | ||
|
|
c247cf888b | ||
|
|
7409c140d4 | ||
|
|
7f4d553201 | ||
|
|
899e468f6a | ||
|
|
faee8d6c5e | ||
|
|
171f966421 | ||
|
|
3f76a413f8 | ||
|
|
61d5680c8d | ||
|
|
9e649d8522 | ||
|
|
c0f736736e | ||
|
|
bcd02df6fd | ||
|
|
7a71c24d5c | ||
|
|
9186f13458 | ||
|
|
c48e157a25 | ||
|
|
84ca0ada1b | ||
|
|
331798babc | ||
|
|
93fed0ce86 | ||
|
|
87703c6511 | ||
|
|
de0dfbca9a | ||
|
|
8eddc09bba | ||
|
|
0f263bcff2 | ||
|
|
a9b2f4885e | ||
|
|
a94292a69f | ||
|
|
5b072c88a6 | ||
|
|
3fb9033fb7 | ||
|
|
d9c1b58fc3 | ||
|
|
f93d1a2633 | ||
|
|
bc53365620 | ||
|
|
b8281c34e2 | ||
|
|
81ff16248e | ||
|
|
8bb085a055 | ||
|
|
c72dab49f4 | ||
|
|
feec16b682 | ||
|
|
feb64cb9b5 | ||
|
|
97d3e8648b | ||
|
|
b1095bb91b | ||
|
|
b3c1df8561 | ||
|
|
2221bf1cba | ||
|
|
8799af9747 | ||
|
|
20c4487853 | ||
|
|
f609012f02 | ||
|
|
171df3aba5 | ||
|
|
0ae2d61974 | ||
|
|
9e824fc4fe | ||
|
|
b6b47d6683 | ||
|
|
1d818e1510 | ||
|
|
b00fa1dfee | ||
|
|
ec3b281a3b | ||
|
|
9318e4f0e8 | ||
|
|
16e8a00f45 | ||
|
|
2bb79c34c3 | ||
|
|
eead6d46ff | ||
|
|
15106b49b6 | ||
|
|
452e4dcbad | ||
|
|
96d44e362d | ||
|
|
5a17171bd1 | ||
|
|
777daf6aee | ||
|
|
9b866e8ee4 | ||
|
|
cbec989a7c | ||
|
|
dd5d144b55 | ||
|
|
6157fed71c | ||
|
|
9b8623dd64 | ||
|
|
84647a1c73 | ||
|
|
1a4297ac35 | ||
|
|
236b502155 | ||
|
|
830edb8f52 | ||
|
|
44e1ca9d05 | ||
|
|
6a577438aa | ||
|
|
f043b46696 | ||
|
|
a509568110 | ||
|
|
6183b38089 | ||
|
|
87f337b605 | ||
|
|
281431e041 | ||
|
|
be65cd4df6 | ||
|
|
965dbb6d2b | ||
|
|
07d4725810 | ||
|
|
e36b860a79 | ||
|
|
1e7c5ec0cb | ||
|
|
1abe1a1fd5 | ||
|
|
f782900a15 | ||
|
|
fc2ec7c495 | ||
|
|
ad11208109 | ||
|
|
f63a9dbf9b | ||
|
|
8fb9228871 | ||
|
|
617a24d61e | ||
|
|
6fad501aa0 | ||
|
|
bd89c19c98 | ||
|
|
5bccf9f837 | ||
|
|
22eaa14179 | ||
|
|
d3d0aacd4c | ||
|
|
69f5be65f6 | ||
|
|
508efd1322 | ||
|
|
ecc2de2709 | ||
|
|
2e3b19ebb2 | ||
|
|
050bb1eef5 | ||
|
|
42923656ee | ||
|
|
0acdae02c1 | ||
|
|
567fb8abd1 | ||
|
|
8306d6fdb6 | ||
|
|
20709f1b3e | ||
|
|
52860cc566 | ||
|
|
b32e4daf2b | ||
|
|
2b80c009b3 | ||
|
|
8da1348a48 | ||
|
|
9a33b877f5 | ||
|
|
c8881a9358 | ||
|
|
f6047d8491 | ||
|
|
0d2115197e | ||
|
|
552b5a79ce | ||
|
|
bb3eb81170 | ||
|
|
7ab69cfe5f | ||
|
|
43d32eb10e | ||
|
|
697fdf5bc3 | ||
|
|
2f7f9c0bac | ||
|
|
8fbfe14c63 | ||
|
|
38edc46c46 | ||
|
|
dd34bbe570 | ||
|
|
11622f81f3 | ||
|
|
e998f063a9 | ||
|
|
c289fef3b5 | ||
|
|
f8839957da | ||
|
|
e271a5cba0 | ||
|
|
a486e2962e | ||
|
|
18f94765f7 | ||
|
|
abe5384449 | ||
|
|
698b7a9d00 | ||
|
|
52761affe2 | ||
|
|
1fb5586f56 | ||
|
|
ef2cddd338 | ||
|
|
dbe14d6fe4 | ||
|
|
7e85a123b2 | ||
|
|
ce90d427e8 | ||
|
|
0eb4556c83 | ||
|
|
45fa2eb542 | ||
|
|
2adab1d36f | ||
|
|
162d1ba472 | ||
|
|
4889950a9e | ||
|
|
7d871f6bb5 | ||
|
|
70bac1ab17 | ||
|
|
0caaff0758 | ||
|
|
4094fbd6c5 | ||
|
|
48a4effdb6 | ||
|
|
68ea528841 | ||
|
|
4aa7666e46 | ||
|
|
1d3da4d49a | ||
|
|
a85f6f54fe | ||
|
|
da08b4eeb9 | ||
|
|
b4f291d0ae | ||
|
|
4f9a7f5065 | ||
|
|
1359a77e72 | ||
|
|
3b7c38da10 | ||
|
|
179cb1e6e5 | ||
|
|
435c35414f | ||
|
|
0954aca758 | ||
|
|
2e9e355329 | ||
|
|
6b8826f234 | ||
|
|
b112d88767 | ||
|
|
b06bd825c1 | ||
|
|
b084433158 | ||
|
|
6d777cdc68 | ||
|
|
ebce6be23f | ||
|
|
e432ae98a9 | ||
|
|
09114e6a7b | ||
|
|
ea01fa135e | ||
|
|
b0c9fbe9fc | ||
|
|
0758192c53 | ||
|
|
d2cb659220 | ||
|
|
d0dbfa1cbd | ||
|
|
b9b2fc5773 | ||
|
|
0a4a09352a | ||
|
|
e8fd2eeaa2 | ||
|
|
51545234d3 | ||
|
|
035758f47d | ||
|
|
6dc99f95c0 | ||
|
|
f440795809 | ||
|
|
eeb322bd0e | ||
|
|
9a5aa8eeb9 | ||
|
|
1f967f7e9d | ||
|
|
3c386c2062 | ||
|
|
1d6c50c9a1 | ||
|
|
e589c5b6ef | ||
|
|
6638b391ff | ||
|
|
e3cceb894b | ||
|
|
a649001886 | ||
|
|
39e3746a62 | ||
|
|
8c57e9f8c8 | ||
|
|
5f62fc5a99 | ||
|
|
13bebe71b0 | ||
|
|
9702a192d7 | ||
|
|
a051ddee6d | ||
|
|
be09700d8b | ||
|
|
757dd3f0b6 | ||
|
|
086c36fca6 | ||
|
|
a7d32b580f | ||
|
|
43d75b96c6 | ||
|
|
3734289983 | ||
|
|
96c19cd990 | ||
|
|
08a6f4e6d3 | ||
|
|
28cbce4356 | ||
|
|
3ada520618 | ||
|
|
d4bc3d182f | ||
|
|
4196986273 | ||
|
|
9d6f3180d4 |
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.51.0"
|
||||
__version__ = "15.54.4"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -481,6 +481,7 @@ def get_account_autoname(account_number, account_name, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
if not account:
|
||||
return
|
||||
@@ -501,7 +502,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
if old_name and not from_descendant:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -542,6 +543,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -595,3 +597,31 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
last_gl_update = None
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if not last_gl_update:
|
||||
return
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,11 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
self.validate_doctype()
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_doctype(self):
|
||||
if self.document_type in (
|
||||
*core_doctypes_list,
|
||||
"Accounting Dimension",
|
||||
@@ -49,6 +54,7 @@ class AccountingDimension(Document):
|
||||
"Accounting Dimension Detail",
|
||||
"Company",
|
||||
"Account",
|
||||
"Finance Book",
|
||||
):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
@@ -61,9 +67,6 @@ 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):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
@@ -102,6 +105,7 @@ class AccountingDimension(Document):
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
@@ -262,7 +266,7 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
WHERE p.name = c.parent AND p.disabled = 0""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -374,9 +374,36 @@ def auto_reconcile_vouchers(
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transactions,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
bank_transactions,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
|
||||
|
||||
def start_auto_reconcile(
|
||||
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
||||
):
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
@@ -414,7 +441,6 @@ def auto_reconcile_vouchers(
|
||||
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
return reconciled, partially_reconciled
|
||||
|
||||
|
||||
def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestCouponCode(unittest.TestCase):
|
||||
item_code="_Test Tesla Car",
|
||||
rate=5000,
|
||||
qty=1,
|
||||
do_not_submit=True,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertEqual(so.items[0].rate, 5000)
|
||||
|
||||
@@ -279,7 +279,8 @@
|
||||
{
|
||||
"fieldname": "transaction_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Transaction Exchange Rate"
|
||||
"label": "Transaction Exchange Rate",
|
||||
"precision": "9"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_transaction_currency",
|
||||
@@ -357,7 +358,7 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-22 13:03:39.997475",
|
||||
"modified": "2025-02-21 14:36:49.431166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
|
||||
@@ -129,7 +129,7 @@ class GLEntry(Document):
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if not (self.party_type and self.party):
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
|
||||
@@ -124,3 +124,20 @@ class TestGLEntry(unittest.TestCase):
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
def test_validate_account_party_type_shareholder(self):
|
||||
jv = make_journal_entry(
|
||||
"Opening Balance Equity - _TC",
|
||||
"Cash - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Shareholder"
|
||||
break
|
||||
|
||||
jv.save().submit()
|
||||
self.assertEqual(1, jv.docstatus)
|
||||
|
||||
@@ -141,6 +141,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
@@ -1059,14 +1060,15 @@ class JournalEntry(AccountsController):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
self.transaction_currency = company_currency
|
||||
self.transaction_exchange_rate = 1
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
# Journal assumes the first foreign currency as transaction currency
|
||||
self.transaction_currency = row.account_currency
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
@@ -1091,6 +1093,18 @@ class JournalEntry(AccountsController):
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
|
||||
@@ -575,7 +575,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
order_by="account",
|
||||
)
|
||||
expected = [
|
||||
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
|
||||
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 85.0},
|
||||
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -591,13 +591,14 @@ def make_journal_entry(
|
||||
save=True,
|
||||
submit=False,
|
||||
project=None,
|
||||
company=None,
|
||||
):
|
||||
if not cost_center:
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = posting_date or nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.company = company or "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.multi_currency = 1
|
||||
jv.set(
|
||||
|
||||
@@ -258,6 +258,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
},
|
||||
|
||||
validate: async function (frm) {
|
||||
await frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
|
||||
@@ -812,27 +816,41 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
paid_amount: function (frm) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
|
||||
received_amount: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
|
||||
if (!frm.doc.paid_amount && frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
@@ -1879,8 +1897,6 @@ function prompt_for_missing_account(frm, account) {
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
"label": "Accounts"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "party",
|
||||
"fieldname": "party_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -253,6 +254,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "paid_from",
|
||||
"fieldname": "paid_from_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -286,6 +288,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "paid_to",
|
||||
"fieldname": "paid_to_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -806,7 +809,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-13 16:03:47.169699",
|
||||
"modified": "2025-01-31 17:27:28.555246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -25,6 +25,10 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||
get_party_account_based_on_invoice_discounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
@@ -114,6 +118,23 @@ class PaymentEntry(AccountsController):
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
validate_docs_for_voucher_types(["Payment Entry"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||
return
|
||||
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"references": [], "taxes": [], "deductions": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_liability_account(self):
|
||||
# Auto setting liability account should only be done during 'draft' status
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
@@ -1196,15 +1217,22 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
|
||||
def set_transaction_currency_and_rate(self):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
self.transaction_currency = company_currency
|
||||
self.transaction_exchange_rate = 1
|
||||
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.transaction_currency = self.paid_from_account_currency
|
||||
self.transaction_exchange_rate = self.source_exchange_rate
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.transaction_currency = self.paid_to_account_currency
|
||||
self.transaction_exchange_rate = self.target_exchange_rate
|
||||
|
||||
def build_gl_map(self):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.currency = self.paid_from_account_currency
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.currency = self.paid_to_account_currency
|
||||
self.set_transaction_currency_and_rate()
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
@@ -1283,6 +1311,9 @@ class PaymentEntry(AccountsController):
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if self.transaction_currency == self.party_account_currency
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
@@ -1327,6 +1358,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr + "_in_transaction_currency": self.unallocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_unallocated_amount / self.transaction_exchange_rate,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=self,
|
||||
@@ -1344,6 +1378,7 @@ class PaymentEntry(AccountsController):
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
):
|
||||
self.set_transaction_currency_and_rate()
|
||||
gl_entries = []
|
||||
self.add_advance_gl_entries(gl_entries, entry)
|
||||
|
||||
@@ -1423,9 +1458,16 @@ class PaymentEntry(AccountsController):
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
base_allocated_amount = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr] = base_allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_transaction_currency"] = (
|
||||
invoice.allocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_allocated_amount / self.transaction_exchange_rate
|
||||
)
|
||||
|
||||
args_dict.update(
|
||||
{
|
||||
"against_voucher_type": invoice.reference_doctype,
|
||||
@@ -1443,8 +1485,13 @@ class PaymentEntry(AccountsController):
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = 0
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
args_dict["account"] = self.party_account
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr] = base_allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_transaction_currency"] = (
|
||||
invoice.allocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_allocated_amount / self.transaction_exchange_rate
|
||||
)
|
||||
args_dict.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
@@ -1466,6 +1513,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type == "Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount
|
||||
if self.paid_from_account_currency == self.transaction_currency
|
||||
else self.base_paid_amount / self.transaction_exchange_rate,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
@@ -1481,6 +1531,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type == "Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit_in_transaction_currency": self.received_amount
|
||||
if self.paid_to_account_currency == self.transaction_currency
|
||||
else self.base_received_amount / self.transaction_exchange_rate,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -1516,6 +1569,8 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": d.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
@@ -1541,6 +1596,8 @@ class PaymentEntry(AccountsController):
|
||||
rev_dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
rev_dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
@@ -1563,6 +1620,7 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": account_currency,
|
||||
"against": self.party or self.paid_from,
|
||||
"debit_in_account_currency": d.amount,
|
||||
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
||||
"debit": d.amount,
|
||||
"cost_center": d.cost_center,
|
||||
},
|
||||
@@ -1822,7 +1880,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
@@ -2833,7 +2891,9 @@ def get_payment_entry(
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||
pe.bank_account = frappe.db.get_value(
|
||||
"Bank Account", {"is_company_account": 1, "is_default": 1, "company": doc.company}, "name"
|
||||
)
|
||||
|
||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||
pe.project = doc.get("project") or reduce(
|
||||
@@ -3316,13 +3376,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||
"""Add loss on income discount in base currency."""
|
||||
precision = doc.precision("total")
|
||||
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(base_loss_on_income, precision),
|
||||
"amount": flt(base_loss_on_income, precision) * positive_negative,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3334,6 +3395,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
tax_discount_loss = {}
|
||||
base_total_tax_loss = 0
|
||||
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
@@ -3356,7 +3418,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
"amount": flt(loss, precision) * positive_negative,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -282,6 +282,48 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
|
||||
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
create_payment_terms_template_with_discount()
|
||||
pi.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
|
||||
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Service Tax",
|
||||
"rate": 18,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
|
||||
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
|
||||
self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
|
||||
self.assertEqual(pe_with_tax_loss.difference_amount, 0)
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe.payment_type, "Pay")
|
||||
self.assertEqual(pe.references[0].allocated_amount, 295.0)
|
||||
self.assertEqual(pe.paid_amount, 265.5)
|
||||
self.assertEqual(pe.deductions[0].amount, -29.5)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount(self):
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
create_payment_terms_template_with_discount()
|
||||
|
||||
@@ -313,6 +313,7 @@ class PaymentRequest(Document):
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
"payment_gateway": self.payment_gateway,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -767,26 +768,39 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
||||
|
||||
|
||||
def get_existing_paid_amount(doctype, name):
|
||||
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PLE = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PL)
|
||||
frappe.qb.from_(PLE)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
(PLE.against_voucher_type == PER.reference_doctype)
|
||||
& (PLE.against_voucher_no == PER.reference_name)
|
||||
& (PLE.voucher_type == PER.parenttype)
|
||||
& (PLE.voucher_no == PER.parent)
|
||||
)
|
||||
.select(
|
||||
Abs(Sum(PLE.amount)).as_("total_amount"),
|
||||
Abs(Sum(frappe.qb.terms.Case().when(PER.payment_request.isnotnull(), PLE.amount).else_(0))).as_(
|
||||
"request_paid_amount"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(PLE.voucher_type.isin([doctype, "Journal Entry", "Payment Entry"]))
|
||||
& (PLE.against_voucher_type == doctype)
|
||||
& (PLE.against_voucher_no == name)
|
||||
& (PLE.delinked == 0)
|
||||
& (PLE.docstatus == 1)
|
||||
& (PLE.amount < 0)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
.where(PL.against_voucher_no.eq(name))
|
||||
.where(PL.amount < 0)
|
||||
.where(PL.delinked == 0)
|
||||
.where(PER.docstatus == 1)
|
||||
.where(PER.payment_request.isnull())
|
||||
)
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
result = query.run()
|
||||
ledger_amount = flt(result[0][0]) if result else 0
|
||||
request_paid_amount = flt(result[0][1]) if result else 0
|
||||
|
||||
return ledger_amount - request_paid_amount
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
|
||||
@@ -542,6 +542,73 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_more_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300)
|
||||
pi_1.submit()
|
||||
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
|
||||
pr.grand_total = 200
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
pr_1 = make_payment_request(
|
||||
dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1
|
||||
)
|
||||
pr_1.grand_total = 200
|
||||
pr_1.submit()
|
||||
pr_1.create_payment_entry()
|
||||
|
||||
pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].reference_doctype = pi.doctype
|
||||
pe.references[0].reference_name = pi.name
|
||||
pe.references[0].grand_total = pi.grand_total
|
||||
pe.references[0].outstanding_amount = pi.outstanding_amount
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": pi_1.doctype,
|
||||
"reference_name": pi_1.name,
|
||||
"grand_total": pi_1.grand_total,
|
||||
"outstanding_amount": pi_1.outstanding_amount,
|
||||
"allocated_amount": 100,
|
||||
},
|
||||
)
|
||||
|
||||
pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
pi.load_from_db()
|
||||
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
|
||||
|
||||
def test_consider_journal_entry_and_return_invoice(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
si = create_sales_invoice(currency="INR", qty=5, rate=500)
|
||||
|
||||
je = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 500, save=False)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = si.customer
|
||||
je.accounts[1].reference_type = "Sales Invoice"
|
||||
je.accounts[1].reference_name = si.name
|
||||
je.accounts[1].credit_in_account_currency = 500
|
||||
je.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.paid_amount = 500
|
||||
pe.references[0].allocated_amount = 500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
cr_note = create_sales_invoice(qty=-1, rate=500, is_return=1, return_against=si.name, do_not_save=1)
|
||||
cr_note.update_outstanding_for_self = 0
|
||||
cr_note.save()
|
||||
cr_note.submit()
|
||||
|
||||
si.load_from_db()
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
|
||||
@@ -27,6 +27,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -39,6 +40,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
@@ -156,6 +158,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date="2021-03-15",
|
||||
company=company,
|
||||
)
|
||||
jv.company = company
|
||||
jv.finance_book = create_finance_book().name
|
||||
@@ -198,6 +201,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -220,6 +224,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center1,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -232,6 +237,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
@@ -261,6 +267,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -68,6 +70,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -86,10 +89,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
# make return entry of pos_inv2
|
||||
@@ -111,10 +116,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -165,6 +172,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
# if in between a mandatory accounting dimension is added to the POS Profile then
|
||||
@@ -218,11 +226,27 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv.payments[0].amount = pos_inv.grand_total
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv2.payments[0].amount = pos_inv2.grand_total
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
@@ -1623,6 +1623,5 @@
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -477,6 +482,20 @@ class POSInvoice(SalesInvoice):
|
||||
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
def validate_full_payment(self):
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get("amended_from"):
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -313,7 +313,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
|
||||
)
|
||||
|
||||
pos.insert()
|
||||
@@ -324,6 +324,11 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.set("payments", [])
|
||||
pos_return1.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return1.paid_amount = -1000
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
@@ -338,6 +343,11 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.set("payments", [])
|
||||
pos_return2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return2.paid_amount = -1000
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
@@ -373,6 +383,15 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
inv.payments = []
|
||||
self.assertRaises(frappe.ValidationError, inv.insert)
|
||||
|
||||
def test_partial_payment(self):
|
||||
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
|
||||
)
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
|
||||
def test_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -581,7 +600,13 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
)
|
||||
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
lpe = frappe.get_doc(
|
||||
"Loyalty Point Entry",
|
||||
@@ -607,7 +632,13 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
# add 10 loyalty points
|
||||
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
pos_inv.paid_amount = 10000
|
||||
pos_inv.submit()
|
||||
|
||||
before_lp_details = get_loyalty_program_details_with_points(
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
@@ -641,10 +672,12 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -676,6 +709,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
||||
@@ -692,6 +726,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -744,6 +779,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -774,7 +810,10 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# POS Invoice 1, for the batch without bundle
|
||||
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
||||
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
|
||||
)
|
||||
pos_inv1.items[0].batch_no = batch_no
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
@@ -790,8 +829,14 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# POS Invoice 2, for the batch with bundle
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1
|
||||
)
|
||||
pos_inv2.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
pos_inv2.reload()
|
||||
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
|
||||
|
||||
@@ -826,6 +871,10 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1 = create_pos_invoice(
|
||||
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
|
||||
)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
|
||||
)
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
|
||||
|
||||
class POSInvoiceMergeLog(Document):
|
||||
@@ -116,17 +119,18 @@ class POSInvoiceMergeLog(Document):
|
||||
returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
sales_invoice, credit_notes = "", {}
|
||||
sales_invoice_doc = None
|
||||
if sales:
|
||||
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice = sales_invoice_doc.name
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
|
||||
distinguished_returns = self.distinguish_return_pos_invoices(returns, sales_invoice_doc)
|
||||
credit_notes = self.process_merging_into_credit_notes(distinguished_returns)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_notes)
|
||||
|
||||
def on_cancel(self):
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
@@ -156,34 +160,50 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
return sales_invoice
|
||||
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
def process_merging_into_credit_notes(self, data):
|
||||
credit_notes = {}
|
||||
for key, value in data.items():
|
||||
if not value:
|
||||
continue
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, value)
|
||||
credit_note.return_against = key
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
credit_notes[credit_note.name] = [d.name for d in value]
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
return credit_notes
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
def distinguish_return_pos_invoices(self, data, sales_invoice_doc=None):
|
||||
return_invoices = {}
|
||||
|
||||
return credit_note.name
|
||||
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None] = []
|
||||
|
||||
for doc in data:
|
||||
sales_invoices_of_return_against = frappe.db.get_value(
|
||||
"POS Invoice", doc.return_against, "consolidated_invoice"
|
||||
)
|
||||
if sales_invoices_of_return_against:
|
||||
if sales_invoices_of_return_against in return_invoices:
|
||||
return_invoices[sales_invoices_of_return_against].append(doc)
|
||||
else:
|
||||
return_invoices[sales_invoices_of_return_against] = [doc]
|
||||
else:
|
||||
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None].append(doc)
|
||||
|
||||
return return_invoices
|
||||
|
||||
def merge_pos_invoice_into(self, invoice, data):
|
||||
items, payments, taxes = [], [], []
|
||||
@@ -209,33 +229,20 @@ class POSInvoiceMergeLog(Document):
|
||||
loyalty_amount_sum += doc.loyalty_amount
|
||||
|
||||
for item in doc.get("items"):
|
||||
found = False
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
and i.net_rate == item.net_rate
|
||||
and i.warehouse == item.warehouse
|
||||
):
|
||||
found = True
|
||||
i.qty = i.qty + item.qty
|
||||
i.amount = i.amount + item.net_amount
|
||||
i.net_amount = i.amount
|
||||
i.base_amount = i.base_amount + item.base_net_amount
|
||||
i.base_net_amount = i.base_amount
|
||||
|
||||
if not found:
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
si_item.pos_invoice = doc.name
|
||||
si_item.pos_invoice_item = item.name
|
||||
if doc.is_return:
|
||||
si_item.sales_invoice_item = get_sales_invoice_item(
|
||||
doc.return_against, item.pos_invoice_item
|
||||
)
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
found = False
|
||||
@@ -292,22 +299,23 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
accounting_dimensions = required_accounting_dimensions()
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
|
||||
dimension_values = frappe.db.get_value(
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
|
||||
)
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_value = dimension_values.get(dimension)
|
||||
dimension_value = dimension_values.get(dimension.fieldname)
|
||||
|
||||
if not dimension_value:
|
||||
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
|
||||
frappe.throw(
|
||||
_("Please set Accounting Dimension {} in {}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(dimension.label),
|
||||
frappe.get_desk_link("POS Profile", invoice.pos_profile),
|
||||
)
|
||||
)
|
||||
|
||||
invoice.set(dimension, dimension_value)
|
||||
invoice.set(dimension.fieldname, dimension_value)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
@@ -324,16 +332,16 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
return sales_invoice
|
||||
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""):
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_notes=None):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
doc.update(
|
||||
{
|
||||
"consolidated_invoice": None
|
||||
if self.docstatus == 2
|
||||
else (credit_note if doc.is_return else sales_invoice)
|
||||
}
|
||||
)
|
||||
inv = sales_invoice
|
||||
if doc.is_return:
|
||||
for key, value in credit_notes.items():
|
||||
if doc.name in value:
|
||||
inv = key
|
||||
break
|
||||
doc.update({"consolidated_invoice": None if self.docstatus == 2 else inv})
|
||||
doc.set_status(update=True)
|
||||
doc.save()
|
||||
|
||||
@@ -625,3 +633,26 @@ def get_error_message(message) -> str:
|
||||
return message["message"]
|
||||
except Exception:
|
||||
return str(message)
|
||||
|
||||
|
||||
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
||||
try:
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
SalesInvoiceItem = DocType("Sales Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.from_(SalesInvoiceItem)
|
||||
.select(SalesInvoiceItem.name)
|
||||
.where(
|
||||
(SalesInvoice.name == SalesInvoiceItem.parent)
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return result[0].name if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -28,14 +28,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -61,14 +64,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -122,6 +128,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
inv.insert()
|
||||
inv.payments[0].amount = inv.grand_total
|
||||
inv.save()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
@@ -138,6 +146,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
inv2.insert()
|
||||
inv2.payments[0].amount = inv.grand_total
|
||||
inv2.save()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -272,7 +282,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv2.submit()
|
||||
|
||||
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800})
|
||||
inv3.insert()
|
||||
inv3.submit()
|
||||
|
||||
@@ -280,8 +290,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertNotEqual(consolidated_invoice.status, "Paid")
|
||||
self.assertNotEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertEqual(consolidated_invoice.status, "Paid")
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
@@ -416,6 +426,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -430,6 +441,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"disable_grand_total_to_default_mop",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -382,6 +383,12 @@
|
||||
"fieldname": "print_receipt_on_order_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Receipt on Order Complete"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_grand_total_to_default_mop",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable auto setting Grand Total to default Payment Mode"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -409,7 +416,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-01 11:07:03.161950",
|
||||
"modified": "2025-01-29 13:12:30.796630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub, unscrub
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form, now
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
|
||||
|
||||
class POSProfile(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -36,6 +41,7 @@ class POSProfile(Document):
|
||||
currency: DF.Link
|
||||
customer: DF.Link | None
|
||||
customer_groups: DF.Table[POSCustomerGroup]
|
||||
disable_grand_total_to_default_mop: DF.Check
|
||||
disable_rounded_total: DF.Check
|
||||
disabled: DF.Check
|
||||
expense_account: DF.Link | None
|
||||
@@ -69,15 +75,19 @@ class POSProfile(Document):
|
||||
self.validate_accounting_dimensions()
|
||||
|
||||
def validate_accounting_dimensions(self):
|
||||
acc_dim_names = required_accounting_dimensions()
|
||||
for acc_dim in acc_dim_names:
|
||||
if not self.get(acc_dim):
|
||||
acc_dims = get_checks_for_pl_and_bs_accounts()
|
||||
for acc_dim in acc_dims:
|
||||
if (
|
||||
self.company == acc_dim.company
|
||||
and not self.get(acc_dim.fieldname)
|
||||
and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} is a mandatory Accounting Dimension. <br>"
|
||||
"Please set a value for {0} in Accounting Dimensions section."
|
||||
).format(
|
||||
unscrub(frappe.bold(acc_dim)),
|
||||
frappe.bold(acc_dim.label),
|
||||
),
|
||||
title=_("Mandatory Accounting Dimension"),
|
||||
)
|
||||
@@ -195,17 +205,41 @@ class POSProfile(Document):
|
||||
def get_item_groups(pos_profile):
|
||||
item_groups = []
|
||||
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
|
||||
permitted_item_groups = get_permitted_nodes("Item Group")
|
||||
|
||||
if pos_profile.get("item_groups"):
|
||||
# Get items based on the item groups defined in the POS profile
|
||||
for data in pos_profile.get("item_groups"):
|
||||
item_groups.extend(
|
||||
["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)]
|
||||
[
|
||||
"%s" % frappe.db.escape(d.name)
|
||||
for d in get_child_nodes("Item Group", data.item_group)
|
||||
if not permitted_item_groups or d.name in permitted_item_groups
|
||||
]
|
||||
)
|
||||
|
||||
if not item_groups and permitted_item_groups:
|
||||
item_groups = ["%s" % frappe.db.escape(d) for d in permitted_item_groups]
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
|
||||
def get_permitted_nodes(group_type):
|
||||
nodes = []
|
||||
permitted_nodes = get_permitted_documents(group_type)
|
||||
|
||||
if not permitted_nodes:
|
||||
return nodes
|
||||
|
||||
for node in permitted_nodes:
|
||||
if frappe.db.get_value(group_type, node, "is_group"):
|
||||
nodes.extend([d.name for d in get_child_nodes(group_type, node)])
|
||||
else:
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def get_child_nodes(group_type, root):
|
||||
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
|
||||
return frappe.db.sql(
|
||||
@@ -215,23 +249,6 @@ def get_child_nodes(group_type, root):
|
||||
)
|
||||
|
||||
|
||||
def required_accounting_dimensions():
|
||||
p = frappe.qb.DocType("Accounting Dimension")
|
||||
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
|
||||
acc_dim_doc = (
|
||||
frappe.qb.from_(p)
|
||||
.inner_join(c)
|
||||
.on(p.name == c.parent)
|
||||
.select(c.parent)
|
||||
.where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1))
|
||||
.where(p.disabled == 0)
|
||||
).run(as_dict=1)
|
||||
|
||||
acc_dim_names = [scrub(d.parent) for d in acc_dim_doc]
|
||||
return acc_dim_names
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"column_break_42",
|
||||
"free_item_uom",
|
||||
"round_free_qty",
|
||||
"dont_enforce_free_item_qty",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
@@ -643,12 +644,19 @@
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
|
||||
"fieldname": "dont_enforce_free_item_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Enforce Free Item Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -60,6 +60,7 @@ class PricingRule(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
dont_enforce_free_item_qty: DF.Check
|
||||
for_price_list: DF.Link | None
|
||||
free_item: DF.Link | None
|
||||
free_item_rate: DF.Currency
|
||||
@@ -453,8 +454,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
continue
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
@@ -645,7 +645,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
|
||||
if pricing_rule.margin_type in ["Percentage", "Amount"]:
|
||||
item_details.margin_rate_or_amount = 0.0
|
||||
item_details.margin_type = None
|
||||
elif pricing_rule.get("free_item"):
|
||||
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
|
||||
item_details.remove_free_item = (
|
||||
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
|
||||
)
|
||||
|
||||
@@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase):
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
def test_dont_enforce_free_item_qty(self):
|
||||
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"min_qty": 0,
|
||||
"max_qty": 7,
|
||||
"discount_percentage": 17.5,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 0,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
pricing_rule = frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With enforcement
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 2)
|
||||
|
||||
# Without enforcement
|
||||
pricing_rule.dont_enforce_free_item_qty = 1
|
||||
pricing_rule.save()
|
||||
|
||||
# Test 2 : Deleted free item will not be fetched again on save without enforcement
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_cumulative_pricing_rule(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
|
||||
test_record = {
|
||||
@@ -1451,6 +1499,7 @@ def make_pricing_rule(**args):
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
|
||||
args.pop((item.item_code, item.pricing_rules))
|
||||
|
||||
for free_item in args.values():
|
||||
doc.append("items", free_item)
|
||||
if doc.is_new() or not frappe.get_value(
|
||||
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
|
||||
):
|
||||
doc.append("items", free_item)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
@@ -236,17 +236,21 @@ def get_ar_filters(doc, entry):
|
||||
|
||||
def get_html(doc, filters, entry, col, res, ageing):
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
if doc.report == "General Ledger"
|
||||
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
)
|
||||
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
if doc.report == "General Ledger":
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
)
|
||||
|
||||
process_soa_html = frappe.get_hooks("process_soa_html")
|
||||
# fetching custom print format for Process Statement of Accounts
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
@@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
|
||||
@@ -871,6 +871,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_payment_gl_entries(gl_entries)
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
@@ -916,6 +917,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"credit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
@@ -951,7 +953,7 @@ class PurchaseInvoice(BuyingController):
|
||||
valuation_tax_accounts = [
|
||||
d.account_head
|
||||
for d in self.get("taxes")
|
||||
if d.category in ("Valuation", "Total and Valuation")
|
||||
if d.category in ("Valuation", "Valuation and Total")
|
||||
and flt(d.base_tax_amount_after_discount_amount)
|
||||
]
|
||||
|
||||
@@ -967,7 +969,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
@@ -976,6 +977,7 @@ class PurchaseInvoice(BuyingController):
|
||||
and self.auto_accounting_for_stock
|
||||
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||
):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
# warehouse account
|
||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
@@ -991,6 +993,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[item.warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1011,6 +1014,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[item.from_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1025,6 +1029,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
@@ -1042,6 +1047,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
warehouse_debit_amount / self.conversion_rate,
|
||||
item.precision("net_amount"),
|
||||
),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1054,7 +1063,9 @@ class PurchaseInvoice(BuyingController):
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1062,8 +1073,9 @@ class PurchaseInvoice(BuyingController):
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
@@ -1086,6 +1098,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.rm_supp_cost),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[self.supplier_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1099,7 +1112,8 @@ class PurchaseInvoice(BuyingController):
|
||||
else item.deferred_expense_account
|
||||
)
|
||||
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
account_currency = get_account_currency(expense_account)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
@@ -1110,7 +1124,8 @@ class PurchaseInvoice(BuyingController):
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": amount,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
@@ -1184,6 +1199,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": stock_rbnb,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||
"debit_in_transaction_currency": flt(
|
||||
item.item_tax_amount / self.conversion_rate,
|
||||
item.precision("item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
"cost_center": self.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1299,6 +1318,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1332,6 +1352,7 @@ class PurchaseInvoice(BuyingController):
|
||||
dr_or_cr + "_in_account_currency": base_amount
|
||||
if account_currency == self.company_currency
|
||||
else amount,
|
||||
dr_or_cr + "_in_transaction_currency": amount,
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1378,6 +1399,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": applicable_amount,
|
||||
"credit_in_transaction_currency": flt(
|
||||
applicable_amount / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
@@ -1396,6 +1421,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": valuation_tax[tax.name],
|
||||
"credit_in_transaction_currency": flt(
|
||||
valuation_tax[tax.name] / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
@@ -1411,6 +1440,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": self.unrealized_profit_loss_account,
|
||||
"against": self.supplier,
|
||||
"credit": flt(self.total_taxes_and_charges),
|
||||
"credit_in_transaction_currency": flt(self.total_taxes_and_charges),
|
||||
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -1460,6 +1490,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"debit_in_account_currency": self.base_paid_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"debit_in_transaction_currency": self.paid_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1481,6 +1512,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": self.base_paid_amount
|
||||
if bank_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
bank_account_currency,
|
||||
@@ -1505,6 +1537,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"debit_in_account_currency": self.base_write_off_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"debit_in_transaction_currency": self.write_off_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1525,6 +1558,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": self.base_write_off_amount
|
||||
if write_off_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"credit_in_transaction_currency": self.write_off_amount,
|
||||
"cost_center": self.cost_center or self.write_off_cost_center,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -2482,6 +2482,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 5300
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before)
|
||||
self.assertEqual(incoming_rate, 53000)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 9
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower
|
||||
self.assertEqual(incoming_rate, 50350)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 12
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher
|
||||
self.assertEqual(incoming_rate, 54766.667)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
@@ -2569,6 +2639,50 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
|
||||
)
|
||||
|
||||
def test_trx_currency_debit_credit_for_high_precision(self):
|
||||
exc_rate = 0.737517516
|
||||
pi = make_purchase_invoice(
|
||||
currency="USD", conversion_rate=exc_rate, qty=1, rate=2000, do_not_save=True
|
||||
)
|
||||
pi.supplier = "_Test Supplier USD"
|
||||
pi.save().submit()
|
||||
|
||||
expected = (
|
||||
("_Test Account Cost for Goods Sold - _TC", 1475.04, 0.0, 2000.0, 0.0, "USD", exc_rate),
|
||||
("_Test Payable USD - _TC", 0.0, 1475.04, 0.0, 2000.0, "USD", exc_rate),
|
||||
)
|
||||
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name},
|
||||
fields=[
|
||||
"account",
|
||||
"debit",
|
||||
"credit",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_currency",
|
||||
"transaction_exchange_rate",
|
||||
],
|
||||
order_by="account",
|
||||
as_list=1,
|
||||
)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
||||
|
||||
invoice = make_purchase_invoice(qty=10)
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = -10
|
||||
return_doc.save().submit()
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = 0
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -461,7 +461,8 @@
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No"
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
@@ -975,7 +976,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-28 15:06:19.246141",
|
||||
"modified": "2025-03-12 16:33:12.453290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@@ -985,4 +986,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -897,8 +897,16 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
|
||||
project: function (frm) {
|
||||
if (frm.doc.project) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project,
|
||||
frappe.call({
|
||||
method: "is_auto_fetch_timesheet_enabled",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
if (cint(r.message)) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -914,9 +922,25 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
}
|
||||
|
||||
const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
|
||||
|
||||
if (kwargs.item_code) {
|
||||
frm.events.add_timesheet_item(frm, kwargs.item_code, timesheets);
|
||||
}
|
||||
|
||||
return frm.events.set_timesheet_data(frm, timesheets);
|
||||
},
|
||||
|
||||
add_timesheet_item: function (frm, item_code, timesheets) {
|
||||
const row = frm.add_child("items");
|
||||
frappe.model.set_value(row.doctype, row.name, "item_code", item_code);
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
row.name,
|
||||
"qty",
|
||||
timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0)
|
||||
);
|
||||
},
|
||||
|
||||
async get_timesheet_data(frm, kwargs) {
|
||||
return frappe
|
||||
.call({
|
||||
@@ -1014,6 +1038,22 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
@@ -1038,6 +1078,7 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2022-01-25 10:29:57.771398",
|
||||
"default_print_format": "Sales Invoice Print",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -299,7 +300,8 @@
|
||||
"oldfieldname": "project_name",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -2176,6 +2178,7 @@
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
@@ -2186,7 +2189,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-14 11:38:30.446370",
|
||||
"modified": "2025-03-05 17:06:59.720616",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2232,6 +2235,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -460,6 +460,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -561,6 +563,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_reservation_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
@@ -675,7 +678,13 @@ class SalesInvoice(SellingController):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
@@ -1087,11 +1096,15 @@ class SalesInvoice(SellingController):
|
||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||
|
||||
def update_timesheet_billing_for_project(self):
|
||||
if not self.timesheets and self.project:
|
||||
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
|
||||
self.add_timesheet_data()
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_auto_fetch_timesheet_enabled(self):
|
||||
return frappe.db.get_single_value("Projects Settings", "fetch_timesheet_in_sales_invoice")
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_timesheet_data(self):
|
||||
self.set("timesheets", [])
|
||||
@@ -1234,6 +1247,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_customer_gl_entry(self, gl_entries):
|
||||
@@ -1267,6 +1281,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"debit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1298,6 +1313,9 @@ class SalesInvoice(SellingController):
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, tax.precision("tax_amount_after_discount_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
amount, tax.precision("tax_amount_after_discount_amount")
|
||||
),
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1315,6 +1333,7 @@ class SalesInvoice(SellingController):
|
||||
"against": self.customer,
|
||||
"debit": flt(self.total_taxes_and_charges),
|
||||
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
|
||||
"debit_in_transaction_currency": flt(self.total_taxes_and_charges),
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1413,6 +1432,7 @@ class SalesInvoice(SellingController):
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
@@ -1464,6 +1484,7 @@ class SalesInvoice(SellingController):
|
||||
+ cstr(self.loyalty_redemption_account)
|
||||
+ " for the Loyalty Program",
|
||||
"credit": self.loyalty_amount,
|
||||
"credit_in_transaction_currency": self.loyalty_amount,
|
||||
"against_voucher": self.return_against if cint(self.is_return) else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1478,6 +1499,7 @@ class SalesInvoice(SellingController):
|
||||
"cost_center": self.cost_center or self.loyalty_redemption_cost_center,
|
||||
"against": self.customer,
|
||||
"debit": self.loyalty_amount,
|
||||
"debit_in_transaction_currency": self.loyalty_amount,
|
||||
"remark": "Loyalty Points redeemed by the customer",
|
||||
},
|
||||
item=self,
|
||||
@@ -1511,6 +1533,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"credit_in_transaction_currency": payment_mode.amount,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1530,6 +1553,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": payment_mode.base_amount
|
||||
if payment_mode_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"debit_in_transaction_currency": payment_mode.amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
payment_mode_account_currency,
|
||||
@@ -1554,6 +1578,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": flt(self.base_change_amount)
|
||||
if self.party_account_currency == self.company_currency
|
||||
else flt(self.change_amount),
|
||||
"debit_in_transaction_currency": flt(self.change_amount),
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1572,6 +1597,7 @@ class SalesInvoice(SellingController):
|
||||
"account": self.account_for_change_amount,
|
||||
"against": self.customer,
|
||||
"credit": self.base_change_amount,
|
||||
"credit_in_transaction_currency": self.change_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -1603,6 +1629,9 @@ class SalesInvoice(SellingController):
|
||||
if self.party_account_currency == self.company_currency
|
||||
else flt(self.write_off_amount, self.precision("write_off_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
self.write_off_amount, self.precision("write_off_amount")
|
||||
),
|
||||
"against_voucher": self.return_against if cint(self.is_return) else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1623,6 +1652,9 @@ class SalesInvoice(SellingController):
|
||||
if write_off_account_currency == self.company_currency
|
||||
else flt(self.write_off_amount, self.precision("write_off_amount"))
|
||||
),
|
||||
"debit_in_transaction_currency": flt(
|
||||
self.write_off_amount, self.precision("write_off_amount")
|
||||
),
|
||||
"cost_center": self.cost_center or self.write_off_cost_center or default_cost_center,
|
||||
},
|
||||
write_off_account_currency,
|
||||
@@ -1667,6 +1699,9 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": flt(
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit": flt(
|
||||
self.base_rounding_adjustment, self.precision("base_rounding_adjustment")
|
||||
),
|
||||
@@ -1930,13 +1965,16 @@ def is_overdue(doc, total):
|
||||
"base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
|
||||
)
|
||||
|
||||
payable_amount = sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
payable_amount = flt(
|
||||
sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
),
|
||||
doc.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
return (total - outstanding_amount) < payable_amount
|
||||
return flt(total - outstanding_amount, doc.precision("outstanding_amount")) < payable_amount
|
||||
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
|
||||
@@ -4246,6 +4246,31 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_total_billed_amount_with_different_projects(self):
|
||||
# This test case is for checking the scenario where project is set at document level and for **some** child items only, not all
|
||||
from copy import copy
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.company = "_Test Company"
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.items.append(copy(si.items[0]))
|
||||
si.items.append(copy(si.items[0]))
|
||||
si.items[0].project = project.name
|
||||
si.items[1].project = project.name
|
||||
# Not setting project on last item
|
||||
si.items[1].insert()
|
||||
si.items[2].insert()
|
||||
si.submit()
|
||||
|
||||
project.reload()
|
||||
self.assertIsNone(si.items[2].project)
|
||||
self.assertEqual(project.total_billed_amount, 300)
|
||||
|
||||
def test_pos_returns_with_party_account_currency(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
@@ -4270,6 +4295,20 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos_return = make_sales_return(pos.name)
|
||||
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
||||
|
||||
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
||||
|
||||
invoice = create_sales_invoice(qty=10)
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = -10
|
||||
return_doc.save().submit()
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = 0
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
"delivery_note",
|
||||
"dn_detail",
|
||||
"delivered_qty",
|
||||
"column_break_vwhb",
|
||||
"pos_invoice",
|
||||
"pos_invoice_item",
|
||||
"internal_transfer_section",
|
||||
"purchase_order",
|
||||
"column_break_92",
|
||||
@@ -630,6 +633,7 @@
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
@@ -945,19 +949,41 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_invoice_item",
|
||||
"fieldtype": "Data",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "POS Invoice Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vwhb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_invoice",
|
||||
"fieldtype": "Link",
|
||||
"label": "POS Invoice",
|
||||
"no_copy": 1,
|
||||
"options": "POS Invoice",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-25 16:27:33.287341",
|
||||
"modified": "2025-03-12 16:33:52.503777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ class SalesInvoiceItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pos_invoice: DF.Link | None
|
||||
pos_invoice_item: DF.Data | None
|
||||
price_list_rate: DF.Currency
|
||||
pricing_rules: DF.SmallText | None
|
||||
project: DF.Link | None
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
taxable_amount: DF.Currency
|
||||
voucher_name: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
voucher_name: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -10,6 +10,7 @@ frappe.ui.form.on("Tax Withholding Category", {
|
||||
filters: {
|
||||
company: child.company,
|
||||
root_type: ["in", ["Asset", "Liability"]],
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,27 +36,38 @@ class TaxWithholdingCategory(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_accounts()
|
||||
self.validate_companies_and_accounts()
|
||||
self.validate_thresholds()
|
||||
|
||||
def validate_dates(self):
|
||||
last_date = None
|
||||
for d in self.get("rates"):
|
||||
last_to_date = None
|
||||
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
|
||||
|
||||
for d in rates:
|
||||
if getdate(d.from_date) >= getdate(d.to_date):
|
||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||
|
||||
# validate overlapping of dates
|
||||
if last_date and getdate(d.to_date) < getdate(last_date):
|
||||
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
|
||||
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
|
||||
|
||||
def validate_accounts(self):
|
||||
existing_accounts = []
|
||||
last_to_date = d.to_date
|
||||
|
||||
def validate_companies_and_accounts(self):
|
||||
existing_accounts = set()
|
||||
companies = set()
|
||||
for d in self.get("accounts"):
|
||||
# validate duplicate company
|
||||
if d.get("company") in companies:
|
||||
frappe.throw(_("Company {0} added multiple times").format(frappe.bold(d.get("company"))))
|
||||
companies.add(d.get("company"))
|
||||
|
||||
# validate duplicate account
|
||||
if d.get("account") in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||
|
||||
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||
existing_accounts.append(d.get("account"))
|
||||
existing_accounts.add(d.get("account"))
|
||||
|
||||
def validate_thresholds(self):
|
||||
for d in self.get("rates"):
|
||||
@@ -270,7 +281,10 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
parties,
|
||||
tax_details,
|
||||
inv.company,
|
||||
party_type=party_type,
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
@@ -360,11 +374,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
voucher_wise_amount = []
|
||||
vouchers = []
|
||||
|
||||
ldcs = frappe.db.get_all(
|
||||
"Lower Deduction Certificate",
|
||||
filters={
|
||||
"valid_from": [">=", tax_details.from_date],
|
||||
"valid_upto": ["<=", tax_details.to_date],
|
||||
"company": company,
|
||||
"supplier": ["in", parties],
|
||||
},
|
||||
fields=["supplier", "valid_from", "valid_upto", "rate"],
|
||||
)
|
||||
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = [
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"name",
|
||||
"grand_total",
|
||||
"posting_date",
|
||||
]
|
||||
|
||||
filters = {
|
||||
@@ -383,18 +409,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
||||
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
}
|
||||
)
|
||||
d = frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
"posting_date": d.posting_date,
|
||||
}
|
||||
)
|
||||
|
||||
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
|
||||
if ldc[0].supplier in parties and ldc[0].rate == 0:
|
||||
d.update({"taxable_amount": 0})
|
||||
|
||||
vouchers.append(d.voucher_name)
|
||||
voucher_wise_amount.append(d)
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
|
||||
@@ -416,6 +447,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in journal_entries_details:
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.utils import add_days, add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -519,7 +519,7 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
payment.save().submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
@@ -666,6 +666,49 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def test_ldc_at_0_rate(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
valid_from = fiscal_year[1]
|
||||
valid_upto = add_months(valid_from, 1)
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=0,
|
||||
limit=50000,
|
||||
valid_from=valid_from,
|
||||
valid_upto=valid_upto,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True
|
||||
)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
pi2 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier",
|
||||
rate=35000,
|
||||
posting_date=add_days(valid_upto, 1),
|
||||
set_posting_time=True,
|
||||
)
|
||||
pi2.submit()
|
||||
self.assertEqual(len(pi2.taxes), 1)
|
||||
# pi1 net total shouldn't be included as it lies within LDC at rate of '0'
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
@@ -823,7 +866,8 @@ def create_purchase_invoice(**args):
|
||||
pi = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Invoice",
|
||||
"posting_date": today(),
|
||||
"set_posting_time": args.set_posting_time or False,
|
||||
"posting_date": args.posting_date or today(),
|
||||
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||
"supplier": args.supplier,
|
||||
"company": "_Test Company",
|
||||
@@ -1161,7 +1205,9 @@ def create_tax_withholding_category(
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit):
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
@@ -1172,8 +1218,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"valid_from": valid_from or fiscal_year[1],
|
||||
"valid_upto": valid_upto or fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ def process_debit_credit_difference(gl_map):
|
||||
voucher_no = gl_map[0].voucher_no
|
||||
allowance = get_debit_credit_allowance(voucher_type, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
if not (
|
||||
@@ -441,9 +441,9 @@ def process_debit_credit_difference(gl_map):
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
|
||||
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
|
||||
make_round_off_gle(gl_map, debit_credit_diff, precision)
|
||||
make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
if not (
|
||||
voucher_type == "Journal Entry"
|
||||
@@ -455,14 +455,23 @@ def process_debit_credit_difference(gl_map):
|
||||
|
||||
def get_debit_credit_difference(gl_map, precision):
|
||||
debit_credit_diff = 0.0
|
||||
trx_cur_debit_credit_diff = 0
|
||||
|
||||
for entry in gl_map:
|
||||
entry.debit = flt(entry.debit, precision)
|
||||
entry.credit = flt(entry.credit, precision)
|
||||
debit_credit_diff += entry.debit - entry.credit
|
||||
|
||||
debit_credit_diff = flt(debit_credit_diff, precision)
|
||||
entry.debit_in_transaction_currency = flt(entry.debit_in_transaction_currency, precision)
|
||||
entry.credit_in_transaction_currency = flt(entry.credit_in_transaction_currency, precision)
|
||||
trx_cur_debit_credit_diff += (
|
||||
entry.debit_in_transaction_currency - entry.credit_in_transaction_currency
|
||||
)
|
||||
|
||||
return debit_credit_diff
|
||||
debit_credit_diff = flt(debit_credit_diff, precision)
|
||||
trx_cur_debit_credit_diff = flt(trx_cur_debit_credit_diff, precision)
|
||||
|
||||
return debit_credit_diff, trx_cur_debit_credit_diff
|
||||
|
||||
|
||||
def get_debit_credit_allowance(voucher_type, precision):
|
||||
@@ -489,7 +498,7 @@ def has_opening_entries(gl_map: list) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
@@ -534,6 +543,12 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
"credit": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit_in_transaction_currency": abs(trx_cur_debit_credit_diff)
|
||||
if trx_cur_debit_credit_diff < 0
|
||||
else 0,
|
||||
"credit_in_transaction_currency": trx_cur_debit_credit_diff
|
||||
if trx_cur_debit_credit_diff > 0
|
||||
else 0,
|
||||
"cost_center": round_off_cost_center,
|
||||
"party_type": None,
|
||||
"party": None,
|
||||
@@ -680,11 +695,15 @@ def make_reverse_gl_entries(
|
||||
|
||||
debit_in_account_currency = new_gle.get("debit_in_account_currency", 0)
|
||||
credit_in_account_currency = new_gle.get("credit_in_account_currency", 0)
|
||||
debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0)
|
||||
credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0)
|
||||
|
||||
new_gle["debit"] = credit
|
||||
new_gle["credit"] = debit
|
||||
new_gle["debit_in_account_currency"] = credit_in_account_currency
|
||||
new_gle["credit_in_account_currency"] = debit_in_account_currency
|
||||
new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency
|
||||
new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency
|
||||
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
@@ -572,12 +572,13 @@ def validate_party_accounts(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
|
||||
"""Get due date from `Payment Terms Template`"""
|
||||
due_date = None
|
||||
if (bill_date or posting_date) and party:
|
||||
due_date = bill_date or posting_date
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
if not template_name:
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
|
||||
if template_name:
|
||||
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
||||
@@ -765,7 +766,7 @@ def validate_account_party_type(self):
|
||||
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="right" colspan="3" ><strong>Total (debit) </strong></td>
|
||||
<td class="left" >{{ gl | sum(attribute='debit') }}</td>
|
||||
<td class="left" >{{ gl | sum(attribute='debit') | round(2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="top-bottom" colspan="5"><strong>Credit</strong></td>
|
||||
@@ -61,7 +61,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="right" colspan="3"><strong>Total (credit) </strong></td>
|
||||
<td class="left" >{{ gl | sum(attribute='credit') }}</td>
|
||||
<td class="left" >{{ gl | sum(attribute='credit') | round(2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
{% if print_heading_template %}
|
||||
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{%- if doc.meta.is_submittable and doc.docstatus==2-%}
|
||||
<div class="text-center" document-status="cancelled">
|
||||
<h4 style="margin: 0px;">{{ _("CANCELLED") }}</h4>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{% for page in layout %}
|
||||
<div class="page-break">
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
|
||||
</div>
|
||||
<style>
|
||||
.taxes-section .order-taxes.mt-5{
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
.taxes-section .order-taxes .border-btm.pb-5{
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
.print-format label{
|
||||
color: #74808b;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if not no_letterhead and footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row section-break" style="margin-bottom: 10px;">
|
||||
<div class="col-xs-6 p-0">
|
||||
<div class="col-xs-12 value text-uppercase"><b>{{ doc.customer }}</b></div>
|
||||
<div class="col-xs-12">
|
||||
{{ doc.address_display }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3"></div>
|
||||
<div class="col-xs-3" style="padding-left: 5px;">
|
||||
<div>
|
||||
<div><label>{{ _("Invoice ID") }}</label></div>
|
||||
<div>{{ doc.name }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Invoice Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.posting_date) }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Due Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.due_date) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-break">
|
||||
<table class="table table-bordered table-condensed mb-0" style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||
<colgroup>
|
||||
<col style="width: 5%">
|
||||
<col style="width: 45%">
|
||||
<col style="width: 10%">
|
||||
<col style="width: 20%">
|
||||
<col style="width: 20%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Sr") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Details") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Qty") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Rate") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for item in doc.items %}
|
||||
<tr>
|
||||
<td style="text-align:center">{{ loop.index }}</td>
|
||||
<td>
|
||||
<b>{{ item.item_code }}: {{ item.item_name }}</b>
|
||||
{% if (item.description != item.item_name) %}
|
||||
<br>{{ item.description }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{{ item.get_formatted("qty", 0) }}
|
||||
{{ item.get_formatted("uom", 0) }}
|
||||
</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_rate", doc) }}</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_amount", doc) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<!-- total -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-xs-6">
|
||||
<div>
|
||||
<label>{{ _("Amount in Words") }}</label>
|
||||
{{ doc.in_words }}
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>{{ _("Payment Status") }}</label>
|
||||
{{ doc.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="row section-break">
|
||||
<div class="col-xs-7"><div>{{ _("Sub Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("net_total", doc) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% for d in doc.taxes %}
|
||||
{% if d.tax_amount %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8"><div>{{ _(d.description) }}</div></div>
|
||||
<div class="col-xs-4" style="text-align: right;">{{ d.get_formatted("tax_amount") }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7"><div>{{ _("Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("grand_total", doc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row important data-field">
|
||||
<div class="col-xs-12"><label>{{ _("Terms and Conditions") }}: </label></div>
|
||||
<div class="col-xs-12">{{ doc.terms if doc.terms else '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2025-01-22 16:23:51.012200",
|
||||
"css": "",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en",
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "",
|
||||
"font_size": 14,
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 0.0,
|
||||
"margin_top": 0.0,
|
||||
"modified": "2025-01-22 16:23:51.012200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Print",
|
||||
"owner": "Administrator",
|
||||
"page_number": "Hide",
|
||||
"print_format_builder": 0,
|
||||
"print_format_builder_beta": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(data[1][0].get("outstanding"), 300)
|
||||
self.assertEqual(data[1][0].get("currency"), "USD")
|
||||
|
||||
def test_account_payable_for_debit_note(self):
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.is_return = 1
|
||||
pi.items[0].qty = -1
|
||||
pi = pi.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Supplier",
|
||||
"party": [self.supplier],
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
self.assertEqual(data[1][0].get("invoiced"), 300)
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -267,6 +267,18 @@ class ReceivablePayableReport:
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
if self.is_invoice(ple):
|
||||
# when invoice has is_return marked
|
||||
if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
|
||||
# for Credit Note
|
||||
if row.voucher_type == "Sales Invoice":
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
# for Debit Note
|
||||
else:
|
||||
row.invoiced -= amount
|
||||
row.invoiced_in_account_currency -= amount_in_account_currency
|
||||
return
|
||||
|
||||
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
|
||||
# nosemgrep
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
select name, due_date, po_no, is_return
|
||||
from `tabSales Invoice`
|
||||
where posting_date <= %s
|
||||
and company = %s
|
||||
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
|
||||
# nosemgrep
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
select name, due_date, bill_no, bill_date, is_return
|
||||
from `tabPurchase Invoice`
|
||||
where
|
||||
posting_date <= %s
|
||||
|
||||
@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
|
||||
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
|
||||
[0, 0, 0, 100.0, -100.0, cr_note.name],
|
||||
]
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
si_row = next(
|
||||
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 2)
|
||||
|
||||
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
|
||||
expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
|
||||
|
||||
rows = report[:2]
|
||||
for row in rows:
|
||||
self.assertEqual(
|
||||
expected_data[row.voucher_no],
|
||||
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||
[
|
||||
row.invoiced or row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.remaining_balance,
|
||||
row.future_amount,
|
||||
],
|
||||
)
|
||||
|
||||
pe.cancel()
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ def get_group_by_asset_category_data(filters):
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
@@ -247,6 +248,7 @@ def get_group_by_asset_data(filters):
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
@@ -276,6 +278,7 @@ def get_assets_for_grouped_by_category(filters):
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
@@ -284,6 +287,11 @@ def get_assets_for_grouped_by_category(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -307,7 +315,6 @@ def get_assets_for_grouped_by_category(filters):
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
@@ -319,6 +326,7 @@ def get_assets_for_grouped_by_category(filters):
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -354,6 +362,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
@@ -362,6 +371,11 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -385,7 +399,6 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{finance_book_filter} {condition}
|
||||
@@ -397,6 +410,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -503,6 +517,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -91,6 +91,7 @@ function get_filters() {
|
||||
fieldname: "budget_against_filter",
|
||||
label: __("Dimension Filter"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "budget_against",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -307,6 +307,7 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.where(
|
||||
(inv.docstatus == 1)
|
||||
& (deferred_flag_field == 1)
|
||||
& (inv.company == self.filters.company)
|
||||
& (
|
||||
(
|
||||
(self.period_list[0].from_date >= inv_item.service_start_date)
|
||||
|
||||
@@ -2,5 +2,27 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Delivered Items To Be Billed"] = {
|
||||
filters: [],
|
||||
filters: [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("Company"),
|
||||
},
|
||||
{
|
||||
label: __("As on Date"),
|
||||
fieldname: "posting_date",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
label: __("Delivery Note"),
|
||||
fieldname: "delivery_note",
|
||||
fieldtype: "Link",
|
||||
options: "Delivery Note",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
|
||||
|
||||
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
|
||||
def execute(filters=None):
|
||||
columns = get_column()
|
||||
args = get_args()
|
||||
data = get_ordered_to_be_billed_data(args)
|
||||
data = get_ordered_to_be_billed_data(args, filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -76,13 +77,6 @@ def get_column():
|
||||
"options": "Project",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -92,5 +86,6 @@ def get_args():
|
||||
"party": "customer",
|
||||
"date": "posting_date",
|
||||
"order": "name",
|
||||
"order_by": "desc",
|
||||
"order_by": Order.desc,
|
||||
"reference_field": "delivery_note",
|
||||
}
|
||||
|
||||
@@ -630,7 +630,7 @@ def get_cost_centers_with_children(cost_centers):
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldname": "account" if not cash_flow else "section",
|
||||
"label": _("Account") if not cash_flow else _("Section"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
|
||||
@@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
@@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -534,6 +534,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
keylist.append(gle.get("project"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
@@ -679,10 +680,11 @@ def get_columns(filters):
|
||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
columns.append(
|
||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||
|
||||
@@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -219,15 +219,34 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for src in gross_profit_data.grouped_data:
|
||||
row = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
row.append(filters.currency)
|
||||
group_columns = group_wise_columns.get(scrub(filters.group_by))
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
total_base_amount += src.base_amount or 0.00
|
||||
total_buying_amount += src.buying_amount or 0.00
|
||||
|
||||
row = [src.get(col) for col in group_columns] + [filters.currency]
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
"base_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
|
||||
}
|
||||
|
||||
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
|
||||
data.append(total_row)
|
||||
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.functions import IfNull, Round
|
||||
|
||||
from erpnext import get_default_currency
|
||||
|
||||
|
||||
def get_ordered_to_be_billed_data(args):
|
||||
def get_ordered_to_be_billed_data(args, filters=None):
|
||||
doctype, party = args.get("doctype"), args.get("party")
|
||||
child_tab = doctype + " Item"
|
||||
precision = (
|
||||
@@ -18,47 +19,57 @@ def get_ordered_to_be_billed_data(args):
|
||||
or 2
|
||||
)
|
||||
|
||||
project_field = get_project_field(doctype, party)
|
||||
doctype = frappe.qb.DocType(doctype)
|
||||
child_doctype = frappe.qb.DocType(child_tab)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
Select
|
||||
`{parent_tab}`.name, `{parent_tab}`.{date_field},
|
||||
`{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
|
||||
`{child_tab}`.item_code,
|
||||
`{child_tab}`.base_amount,
|
||||
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)),
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)),
|
||||
(`{child_tab}`.base_amount -
|
||||
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) -
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))),
|
||||
`{child_tab}`.item_name, `{child_tab}`.description,
|
||||
{project_field}, `{parent_tab}`.company
|
||||
from
|
||||
`{parent_tab}`, `{child_tab}`
|
||||
where
|
||||
`{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1
|
||||
and `{parent_tab}`.status not in ('Closed', 'Completed')
|
||||
and `{child_tab}`.amount > 0
|
||||
and (`{child_tab}`.base_amount -
|
||||
round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) -
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0
|
||||
order by
|
||||
`{parent_tab}`.{order} {order_by}
|
||||
""".format(
|
||||
parent_tab="tab" + doctype,
|
||||
child_tab="tab" + child_tab,
|
||||
precision=precision,
|
||||
party=party,
|
||||
date_field=args.get("date"),
|
||||
project_field=project_field,
|
||||
order=args.get("order"),
|
||||
order_by=args.get("order_by"),
|
||||
docname = filters.get(args.get("reference_field"), None)
|
||||
project_field = get_project_field(doctype, child_doctype, party)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.inner_join(child_doctype)
|
||||
.on(doctype.name == child_doctype.parent)
|
||||
.select(
|
||||
doctype.name,
|
||||
doctype[args.get("date")].as_("date"),
|
||||
doctype[party],
|
||||
doctype[party + "_name"],
|
||||
child_doctype.item_code,
|
||||
child_doctype.base_amount.as_("amount"),
|
||||
(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1)).as_("billed_amount"),
|
||||
(child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0)).as_("returned_amount"),
|
||||
(
|
||||
child_doctype.base_amount
|
||||
- (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1))
|
||||
- (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
|
||||
).as_("pending_amount"),
|
||||
child_doctype.item_name,
|
||||
child_doctype.description,
|
||||
project_field,
|
||||
)
|
||||
.where(
|
||||
(doctype.docstatus == 1)
|
||||
& (doctype.status.notin(["Closed", "Completed"]))
|
||||
& (doctype.company == filters.get("company"))
|
||||
& (doctype.posting_date <= filters.get("posting_date"))
|
||||
& (child_doctype.amount > 0)
|
||||
& (
|
||||
child_doctype.base_amount
|
||||
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
|
||||
- (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
|
||||
)
|
||||
> 0
|
||||
)
|
||||
.orderby(doctype[args.get("order")], order=args.get("order_by"))
|
||||
)
|
||||
|
||||
if docname:
|
||||
query = query.where(doctype.name == docname)
|
||||
|
||||
def get_project_field(doctype, party):
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_project_field(doctype, child_doctype, party):
|
||||
if party == "supplier":
|
||||
doctype = doctype + " Item"
|
||||
return "`tab%s`.project" % (doctype)
|
||||
return child_doctype.project
|
||||
return doctype.project
|
||||
|
||||
@@ -50,6 +50,7 @@ function get_filters() {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -2,5 +2,27 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Received Items To Be Billed"] = {
|
||||
filters: [],
|
||||
filters: [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("Company"),
|
||||
},
|
||||
{
|
||||
label: __("As on Date"),
|
||||
fieldname: "posting_date",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
label: __("Purchase Receipt"),
|
||||
fieldname: "purchase_receipt",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Receipt",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
|
||||
|
||||
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
|
||||
def execute(filters=None):
|
||||
columns = get_column()
|
||||
args = get_args()
|
||||
data = get_ordered_to_be_billed_data(args)
|
||||
data = get_ordered_to_be_billed_data(args, filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -76,13 +77,6 @@ def get_column():
|
||||
"options": "Project",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -92,5 +86,6 @@ def get_args():
|
||||
"party": "supplier",
|
||||
"date": "posting_date",
|
||||
"order": "name",
|
||||
"order_by": "desc",
|
||||
"order_by": Order.desc,
|
||||
"reference_field": "purchase_receipt",
|
||||
}
|
||||
|
||||
@@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = {
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
doctype: "Account",
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
|
||||
|
||||
@@ -35,9 +37,14 @@ def get_data(filters, show_party_name):
|
||||
filters=party_filters,
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
account_filter = []
|
||||
if filters.get("account"):
|
||||
account_filter = get_accounts_with_children(filters.get("account"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
opening_balances = get_opening_balances(filters)
|
||||
balances_within_period = get_balances_within_period(filters)
|
||||
opening_balances = get_opening_balances(filters, account_filter)
|
||||
balances_within_period = get_balances_within_period(filters, account_filter)
|
||||
|
||||
data = []
|
||||
# total_debit, total_credit = 0, 0
|
||||
@@ -89,30 +96,34 @@ def get_data(filters, show_party_name):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_opening_balances(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("opening_debit"),
|
||||
Sum(GL_Entry.credit).as_("opening_credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (
|
||||
(GL_Entry.posting_date < filters.from_date)
|
||||
| ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date))
|
||||
)
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit)
|
||||
@@ -121,31 +132,33 @@ def get_opening_balances(filters):
|
||||
return opening
|
||||
|
||||
|
||||
def get_balances_within_period(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_balances_within_period(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled = 0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
|
||||
and ifnull(is_opening, 'No') = 'No'
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("debit"),
|
||||
Sum(GL_Entry.credit).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (GL_Entry.posting_date >= filters.from_date)
|
||||
& (GL_Entry.posting_date <= filters.to_date)
|
||||
& (GL_Entry.is_opening == "No")
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
balances_within_period = frappe._dict()
|
||||
for d in gle:
|
||||
balances_within_period.setdefault(d.party, [d.debit, d.credit])
|
||||
|
||||
@@ -773,6 +773,8 @@ def update_reference_in_payment_entry(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
)
|
||||
|
||||
# Ledgers will be reposted by Reconciliation tool
|
||||
payment_entry.flags.ignore_reposting_on_reconciliation = True
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
return row, update_advance_paid
|
||||
@@ -1415,7 +1417,7 @@ def repost_gle_for_stock_vouchers(
|
||||
if not warehouse_account:
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers, company=company)
|
||||
if repost_doc and repost_doc.gl_reposting_index:
|
||||
# Restore progress
|
||||
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
|
||||
@@ -1468,7 +1470,9 @@ def _delete_accounting_ledger_entries(voucher_type, voucher_no):
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
|
||||
|
||||
def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
||||
def sort_stock_vouchers_by_posting_date(
|
||||
stock_vouchers: list[tuple[str, str]], company=None
|
||||
) -> list[tuple[str, str]]:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
voucher_nos = [v[1] for v in stock_vouchers]
|
||||
|
||||
@@ -1479,7 +1483,12 @@ def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -
|
||||
.groupby(sle.voucher_type, sle.voucher_no)
|
||||
.orderby(sle.posting_datetime)
|
||||
.orderby(sle.creation)
|
||||
).run(as_dict=True)
|
||||
)
|
||||
|
||||
if company:
|
||||
sles = sles.where(sle.company == company)
|
||||
|
||||
sles = sles.run(as_dict=True)
|
||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||
|
||||
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Accounts Payable",
|
||||
"link_count": 0,
|
||||
"link_to": "Accounts Payable",
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Accounts Payable Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Accounts Payable Summary",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Register",
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Item-wise Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "Item-wise Purchase Register",
|
||||
@@ -133,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Order Analysis",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Order Analysis",
|
||||
@@ -143,7 +143,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Received Items To Be Billed",
|
||||
"link_count": 0,
|
||||
"link_to": "Received Items To Be Billed",
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Supplier Ledger Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Supplier Ledger Summary",
|
||||
|
||||
@@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_receipt) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt");
|
||||
} else {
|
||||
frm.set_value("purchase_receipt", "");
|
||||
frappe.msgprint({
|
||||
@@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_invoice) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice");
|
||||
} else {
|
||||
frm.set_value("purchase_invoice", "");
|
||||
frappe.msgprint({
|
||||
@@ -639,45 +635,35 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
},
|
||||
|
||||
set_values_from_purchase_doc: function (frm, doctype, purchase_doc) {
|
||||
frm.set_value("company", purchase_doc.company);
|
||||
if (purchase_doc.bill_date) {
|
||||
frm.set_value("purchase_date", purchase_doc.bill_date);
|
||||
} else {
|
||||
frm.set_value("purchase_date", purchase_doc.posting_date);
|
||||
}
|
||||
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
|
||||
frm.set_value("available_for_use_date", frm.doc.purchase_date);
|
||||
}
|
||||
const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
let doctype_field = frappe.scrub(doctype);
|
||||
frm.set_value(doctype_field, "");
|
||||
frappe.msgprint({
|
||||
title: __("Invalid {0}", [__(doctype)]),
|
||||
message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => {
|
||||
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||
var purchase_amount = flt(
|
||||
item.valuation_rate * asset_quantity,
|
||||
precision("gross_purchase_amount")
|
||||
);
|
||||
set_values_from_purchase_doc: (frm, doctype) => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc",
|
||||
args: {
|
||||
purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice,
|
||||
item_code: frm.doc.item_code,
|
||||
doctype: doctype,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
let data = r.message;
|
||||
frm.set_value("company", data.company);
|
||||
frm.set_value("purchase_date", data.purchase_date);
|
||||
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("asset_quantity", data.asset_quantity);
|
||||
frm.set_value("cost_center", data.cost_center);
|
||||
|
||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||
frm.set_value("purchase_amount", purchase_amount);
|
||||
frm.set_value("asset_quantity", asset_quantity);
|
||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
|
||||
} else {
|
||||
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
|
||||
}
|
||||
|
||||
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
|
||||
frm.set_df_property("asset_quantity", "read_only", is_editable);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -225,10 +225,9 @@
|
||||
{
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset"
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
@@ -470,8 +469,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "asset_quantity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Asset Quantity",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
"label": "Asset Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
@@ -541,14 +539,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
"label": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
@@ -595,7 +592,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-12-26 14:23:20.968882",
|
||||
"modified": "2025-02-20 14:09:05.421913",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -95,9 +95,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_invoice_item: DF.Data | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
purchase_receipt_item: DF.Data | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -121,6 +121,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.validate_precision()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
self.validate_item()
|
||||
@@ -199,6 +200,38 @@ class Asset(AccountsController):
|
||||
def after_delete(self):
|
||||
add_asset_activity(self.name, _("Asset deleted"))
|
||||
|
||||
def set_purchase_doc_row_item(self):
|
||||
if self.is_existing_asset or self.is_composite_asset:
|
||||
return
|
||||
|
||||
self.purchase_amount = self.gross_purchase_amount
|
||||
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
purchase_doc = self.purchase_receipt or self.purchase_invoice
|
||||
|
||||
if not purchase_doc:
|
||||
return
|
||||
|
||||
linked_item = self.get_linked_item(purchase_doc_type, purchase_doc)
|
||||
|
||||
if linked_item:
|
||||
if purchase_doc_type == "Purchase Receipt":
|
||||
self.purchase_receipt_item = linked_item
|
||||
else:
|
||||
self.purchase_invoice_item = linked_item
|
||||
|
||||
def get_linked_item(self, purchase_doc_type, purchase_doc):
|
||||
purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc)
|
||||
|
||||
for item in purchase_doc.items:
|
||||
if self.asset_quantity > 1:
|
||||
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
elif item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
if self.purchase_invoice or self.purchase_receipt:
|
||||
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
|
||||
@@ -1125,6 +1158,30 @@ def has_active_capitalization(asset):
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
||||
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
||||
|
||||
if not matching_items:
|
||||
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
|
||||
|
||||
first_item = matching_items[0]
|
||||
is_multiple_items = len(matching_items) > 1
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
"is_multiple_items": is_multiple_items,
|
||||
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -444,9 +444,9 @@ def scrap_asset(asset_name):
|
||||
notes = _("This schedule was created when Asset {0} was scrapped.").format(
|
||||
get_link_to_form(asset.doctype, asset.name)
|
||||
)
|
||||
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
if asset.status != "Fully Depreciated":
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
|
||||
|
||||
|
||||
@@ -143,14 +143,19 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
}
|
||||
}
|
||||
|
||||
set_consumed_stock_items_tagged_to_wip_composite_asset(asset) {
|
||||
set_consumed_stock_items_tagged_to_wip_composite_asset(target_asset) {
|
||||
var me = this;
|
||||
|
||||
if (asset) {
|
||||
if (target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
|
||||
args: {
|
||||
asset: asset,
|
||||
params: {
|
||||
target_asset: target_asset,
|
||||
finance_book: me.frm.doc.finance_book,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
},
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
|
||||
@@ -856,7 +856,10 @@ def get_service_item_details(args):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_tagged_to_wip_composite_asset(asset):
|
||||
def get_items_tagged_to_wip_composite_asset(params):
|
||||
if isinstance(params, str):
|
||||
params = json.loads(params)
|
||||
|
||||
fields = [
|
||||
"item_code",
|
||||
"item_name",
|
||||
@@ -871,25 +874,66 @@ def get_items_tagged_to_wip_composite_asset(asset):
|
||||
"amount",
|
||||
"is_fixed_asset",
|
||||
"parent",
|
||||
"name",
|
||||
]
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields
|
||||
"Purchase Receipt Item",
|
||||
filters={"wip_composite_asset": params.get("target_asset"), "docstatus": 1},
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
stock_items = []
|
||||
asset_items = []
|
||||
|
||||
for d in pr_items:
|
||||
if not d.is_fixed_asset:
|
||||
stock_items.append(frappe._dict(d))
|
||||
stock_item = process_stock_item(d)
|
||||
if stock_item:
|
||||
stock_items.append(stock_item)
|
||||
else:
|
||||
asset_details = frappe.db.get_value(
|
||||
"Asset",
|
||||
{"item_code": d.item_code, "purchase_receipt": d.parent},
|
||||
["name as asset", "asset_name"],
|
||||
as_dict=1,
|
||||
)
|
||||
d.update(asset_details)
|
||||
asset_items.append(frappe._dict(d))
|
||||
asset_item = process_fixed_asset(d)
|
||||
if asset_item:
|
||||
asset_items.append(asset_item)
|
||||
|
||||
return stock_items, asset_items
|
||||
|
||||
|
||||
def process_stock_item(d):
|
||||
stock_capitalized = frappe.db.exists(
|
||||
"Asset Capitalization Stock Item",
|
||||
{
|
||||
"purchase_receipt_item": d.name,
|
||||
"parentfield": "stock_items",
|
||||
"parenttype": "Asset Capitalization",
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if stock_capitalized:
|
||||
return None
|
||||
|
||||
stock_item_data = frappe._dict(d)
|
||||
stock_item_data.purchase_receipt_item = d.name
|
||||
return stock_item_data
|
||||
|
||||
|
||||
def process_fixed_asset(d):
|
||||
asset_details = frappe.db.get_value(
|
||||
"Asset",
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"purchase_receipt": d.parent,
|
||||
"status": ("not in", ["Draft", "Scrapped", "Sold", "Capitalized"]),
|
||||
},
|
||||
["name as asset", "asset_name", "company"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if asset_details:
|
||||
asset_details.update(d)
|
||||
asset_details.update(get_consumed_asset_details(asset_details))
|
||||
d.update(asset_details)
|
||||
|
||||
return frappe._dict(d)
|
||||
return None
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
"column_break_3",
|
||||
"warehouse",
|
||||
"section_break_6",
|
||||
"purchase_receipt_item",
|
||||
"stock_qty",
|
||||
"stock_uom",
|
||||
"actual_qty",
|
||||
"column_break_9",
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
"stock_uom",
|
||||
"batch_and_serial_no_section",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
@@ -53,14 +54,14 @@
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qty and Rate"
|
||||
"label": "Purchase Details"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"label": "Quantity",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
@@ -172,18 +173,26 @@
|
||||
{
|
||||
"fieldname": "column_break_mbuv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-26 17:06:22.564438",
|
||||
"modified": "2025-03-05 12:46:01.074742",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Stock Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
|
||||
@@ -23,6 +23,7 @@ class AssetCapitalizationStockItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
purchase_receipt_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
stock_qty: DF.Float
|
||||
|
||||
@@ -404,7 +404,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
|
||||
@@ -898,7 +898,7 @@ def is_po_fully_subcontracted(po_name):
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
@@ -945,7 +945,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
"condition": lambda item: item.qty != item.subcontracted_quantity,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -920,6 +920,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
make_inter_company_purchase_invoice,
|
||||
)
|
||||
@@ -935,8 +936,16 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
prepare_data_for_internal_transfer()
|
||||
supplier = "_Test Internal Supplier 2"
|
||||
|
||||
create_cost_center(
|
||||
cost_center_name="_Test Cost Center for perpetual inventory Account",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
mr = make_material_request(
|
||||
qty=2, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1"
|
||||
qty=2,
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
cost_center="_Test Cost Center for perpetual inventory Account - TCP1",
|
||||
)
|
||||
|
||||
po = create_purchase_order(
|
||||
@@ -1076,9 +1085,9 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
self.assertEqual(po.items[0].subcontracted_quantity, 5)
|
||||
self.assertEqual(po.items[1].subcontracted_quantity, 0)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
@@ -1114,10 +1123,10 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"subcontracted_quantity",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -913,18 +913,20 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldname": "subcontracted_quantity",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified": "2025-03-02 16:58:26.059601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
@@ -932,6 +934,7 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -80,10 +80,10 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
subcontracted_quantity: DF.Float
|
||||
supplier_part_no: DF.Data | None
|
||||
supplier_quotation: DF.Link | None
|
||||
supplier_quotation_item: DF.Link | None
|
||||
|
||||
@@ -394,7 +394,11 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
|
||||
},
|
||||
"Request for Quotation Item": {
|
||||
"doctype": "Supplier Quotation Item",
|
||||
"field_map": {"name": "request_for_quotation_item", "parent": "request_for_quotation"},
|
||||
"field_map": {
|
||||
"name": "request_for_quotation_item",
|
||||
"parent": "request_for_quotation",
|
||||
"project_name": "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
width: "80",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
on_change: (report) => {
|
||||
report.set_filter_value("name", []);
|
||||
report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
width: "80",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
on_change: (report) => {
|
||||
report.set_filter_value("name", []);
|
||||
report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
{
|
||||
fieldname: "name",
|
||||
label: __("Purchase Order"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
width: "80",
|
||||
options: "Purchase Order",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: { docstatus: 1 },
|
||||
};
|
||||
get_data: function (txt) {
|
||||
let filters = { docstatus: 1 };
|
||||
|
||||
const from_date = frappe.query_report.get_filter_value("from_date");
|
||||
const to_date = frappe.query_report.get_filter_value("to_date");
|
||||
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
|
||||
|
||||
return frappe.db.get_link_options("Purchase Order", txt, filters);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -52,8 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
label: __("Status"),
|
||||
fieldtype: "MultiSelectList",
|
||||
width: "80",
|
||||
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
|
||||
get_data: function (txt) {
|
||||
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
|
||||
let status = [
|
||||
"To Pay",
|
||||
"To Bill",
|
||||
"To Receive",
|
||||
"To Receive and Bill",
|
||||
"Completed",
|
||||
"Closed",
|
||||
];
|
||||
let options = [];
|
||||
for (let option of status) {
|
||||
options.push({
|
||||
|
||||
@@ -70,14 +70,16 @@ def get_data(filters):
|
||||
po.company,
|
||||
po_item.name,
|
||||
)
|
||||
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
|
||||
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
|
||||
.groupby(po_item.name)
|
||||
.orderby(po.transaction_date)
|
||||
)
|
||||
|
||||
for field in ("company", "name"):
|
||||
if filters.get(field):
|
||||
query = query.where(po[field] == filters.get(field))
|
||||
if filters.get("company"):
|
||||
query = query.where(po.company == filters.get("company"))
|
||||
|
||||
if filters.get("name"):
|
||||
query = query.where(po.name.isin(filters.get("name")))
|
||||
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))
|
||||
|
||||
@@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Supplier",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Supplier", txt);
|
||||
},
|
||||
@@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
fieldtype: "MultiSelectList",
|
||||
label: __("Supplier Quotation"),
|
||||
fieldname: "supplier_quotation",
|
||||
options: "Supplier Quotation",
|
||||
default: "",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] });
|
||||
|
||||
@@ -170,7 +170,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_qty_is_not_zero()
|
||||
|
||||
if (
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"]
|
||||
and self.get("is_return")
|
||||
and self.get("update_stock")
|
||||
):
|
||||
@@ -194,6 +194,14 @@ class AccountsController(TransactionBase):
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
|
||||
# Need to set taxes based on taxes_and_charges template
|
||||
# before calculating taxes and totals
|
||||
if self.meta.get_field("taxes_and_charges"):
|
||||
self.validate_enabled_taxes_and_charges()
|
||||
self.validate_tax_account_company()
|
||||
|
||||
self.set_taxes_and_charges()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
@@ -204,10 +212,6 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_all_documents_schedule()
|
||||
|
||||
if self.meta.get_field("taxes_and_charges"):
|
||||
self.validate_enabled_taxes_and_charges()
|
||||
self.validate_tax_account_company()
|
||||
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
self.validate_party_account_currency()
|
||||
@@ -252,8 +256,6 @@ class AccountsController(TransactionBase):
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
self.set_taxes_and_charges()
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
self.calculate_paid_amount()
|
||||
# apply tax withholding only if checked and applicable
|
||||
@@ -417,7 +419,7 @@ class AccountsController(TransactionBase):
|
||||
.where(doc_field.fieldname == "company")
|
||||
).run(as_list=True)
|
||||
|
||||
dimension_list = sum(dimension_list, ["Project"])
|
||||
dimension_list = sum(dimension_list, ["Project", "Cost Center"])
|
||||
self.validate_company(dimension_list)
|
||||
|
||||
for child in self.get_all_children() or []:
|
||||
@@ -821,11 +823,15 @@ class AccountsController(TransactionBase):
|
||||
and item.get("use_serial_batch_fields")
|
||||
)
|
||||
):
|
||||
if fieldname == "batch_no" and not item.batch_no:
|
||||
item.set("rate", ret.get("rate"))
|
||||
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||
item.set(fieldname, value)
|
||||
|
||||
if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
|
||||
if ret.get("rate"):
|
||||
item.set("rate", ret.get("rate"))
|
||||
|
||||
if not item.get("price_list_rate") and ret.get("price_list_rate"):
|
||||
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||
|
||||
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
||||
fieldname
|
||||
):
|
||||
@@ -1113,20 +1119,19 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
# Update details in transaction currency
|
||||
gl_dict.update(
|
||||
{
|
||||
"transaction_currency": self.get("currency") or self.company_currency,
|
||||
"transaction_exchange_rate": item.get("exchange_rate", 1)
|
||||
if self.doctype == "Journal Entry" and item
|
||||
else self.get("conversion_rate", 1),
|
||||
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, gl_dict, "debit"
|
||||
),
|
||||
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, gl_dict, "credit"
|
||||
),
|
||||
}
|
||||
)
|
||||
if self.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]:
|
||||
gl_dict.update(
|
||||
{
|
||||
"transaction_currency": self.get("currency") or self.company_currency,
|
||||
"transaction_exchange_rate": self.get("conversion_rate", 1),
|
||||
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, gl_dict, "debit"
|
||||
),
|
||||
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||
account_currency, gl_dict, "credit"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get("against_voucher_type") and self.get("against_voucher_type"):
|
||||
gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})
|
||||
@@ -1903,22 +1908,22 @@ class AccountsController(TransactionBase):
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
frappe.msgprint(
|
||||
_("System will not check over billing since amount for Item {0} in {1} is zero").format(
|
||||
item.item_code, ref_dt
|
||||
),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(
|
||||
flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item)
|
||||
)
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
@@ -2752,6 +2757,11 @@ class AccountsController(TransactionBase):
|
||||
elif self.doctype == "Payment Entry":
|
||||
self.make_advance_payment_ledger_for_payment()
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
|
||||
for x in gl_entries:
|
||||
x["transaction_currency"] = self.currency
|
||||
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@@ -3281,6 +3291,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
|
||||
"posting_date": parent_doc.transaction_date,
|
||||
"tax_category": parent_doc.get("tax_category"),
|
||||
"company": parent_doc.get("company"),
|
||||
"base_net_rate": item.get("base_net_rate"),
|
||||
}
|
||||
|
||||
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
|
||||
|
||||
@@ -333,7 +333,7 @@ class BuyingController(SubcontractingController):
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("rate_difference_with_purchase_invoice"))
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
@@ -779,8 +779,10 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
is_plural = "s" if len(created_assets) != 1 else ""
|
||||
messages.append(
|
||||
_("Asset{} {assets_link} created for {}").format(
|
||||
is_plural, frappe.bold(d.item_code), assets_link=assets_link
|
||||
_("Asset{is_plural} {assets_link} created for {item_code}").format(
|
||||
is_plural=is_plural,
|
||||
assets_link=assets_link,
|
||||
item_code=frappe.bold(d.item_code),
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -837,6 +837,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
"posting_date": valid_from,
|
||||
"tax_category": filters.get("tax_category"),
|
||||
"company": company,
|
||||
"base_net_rate": filters.get("base_net_rate"),
|
||||
}
|
||||
|
||||
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
||||
|
||||
@@ -25,9 +25,6 @@ def validate_return(doc):
|
||||
if doc.return_against:
|
||||
validate_return_against(doc)
|
||||
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and not doc.update_stock:
|
||||
return
|
||||
|
||||
validate_returned_items(doc)
|
||||
|
||||
|
||||
@@ -118,7 +115,7 @@ def validate_returned_items(doc):
|
||||
elif doc.doctype == "Delivery Note":
|
||||
key = (d.item_code, d.get("dn_detail"))
|
||||
|
||||
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
|
||||
if d.item_code and (flt(d.qty) <= 0 or flt(d.get("received_qty")) <= 0):
|
||||
if key not in valid_items:
|
||||
frappe.msgprint(
|
||||
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
|
||||
@@ -160,6 +157,9 @@ def validate_returned_items(doc):
|
||||
|
||||
def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
fields = ["stock_qty"]
|
||||
if (doc.doctype == "Purchase Invoice" or doc.doctype == "Sales Invoice") and not doc.update_stock:
|
||||
fields = ["qty"]
|
||||
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
|
||||
if not args.get("return_qty_from_rejected_warehouse"):
|
||||
fields.extend(["received_qty", "rejected_qty"])
|
||||
@@ -169,13 +169,16 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
already_returned_data = already_returned_items.get(key) or {}
|
||||
|
||||
company_currency = erpnext.get_company_currency(doc.company)
|
||||
stock_qty_precision = get_field_precision(
|
||||
frappe.get_meta(doc.doctype + " Item").get_field("stock_qty"), company_currency
|
||||
field_precision = get_field_precision(
|
||||
frappe.get_meta(doc.doctype + " Item").get_field(
|
||||
"stock_qty" if doc.get("update_stock", "") else "qty"
|
||||
),
|
||||
company_currency,
|
||||
)
|
||||
|
||||
for column in fields:
|
||||
returned_qty = (
|
||||
flt(already_returned_data.get(column, 0), stock_qty_precision)
|
||||
flt(already_returned_data.get(column, 0), field_precision)
|
||||
if len(already_returned_data) > 0
|
||||
else 0
|
||||
)
|
||||
@@ -190,17 +193,17 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
|
||||
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
|
||||
max_returnable_qty = flt(flt(reference_qty, field_precision) - returned_qty, field_precision)
|
||||
label = column.replace("_", " ").title()
|
||||
|
||||
if reference_qty:
|
||||
if flt(args.get(column)) > 0:
|
||||
frappe.throw(_("{0} must be negative in return document").format(label))
|
||||
elif returned_qty >= reference_qty and args.get(column):
|
||||
elif returned_qty >= reference_qty and args.get(column) >= 0:
|
||||
frappe.throw(
|
||||
_("Item {0} has already been returned").format(args.item_code), StockOverReturnError
|
||||
)
|
||||
elif abs(flt(current_stock_qty, stock_qty_precision)) > max_returnable_qty:
|
||||
elif abs(flt(current_stock_qty, field_precision)) > max_returnable_qty:
|
||||
frappe.throw(
|
||||
_("Row # {0}: Cannot return more than {1} for Item {2}").format(
|
||||
args.idx, max_returnable_qty, args.item_code
|
||||
@@ -258,7 +261,7 @@ def get_already_returned_items(doc):
|
||||
|
||||
field = (
|
||||
frappe.scrub(doc.doctype) + "_item"
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"]
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"]
|
||||
else "dn_detail"
|
||||
)
|
||||
data = frappe.db.sql(
|
||||
@@ -770,6 +773,7 @@ def get_return_against_item_fields(voucher_type):
|
||||
"Delivery Note": "dn_detail",
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Subcontracting Receipt": "subcontracting_receipt_item",
|
||||
"POS Invoice": "sales_invoice_item",
|
||||
}
|
||||
return return_against_item_fields[voucher_type]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user