mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-23 04:40:37 +00:00
Compare commits
353 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b4093069c | ||
|
|
53fef46f9a | ||
|
|
1070322695 | ||
|
|
3ea6278eb9 | ||
|
|
e822a74479 | ||
|
|
e36e5027d7 | ||
|
|
f97e29058c | ||
|
|
964f9275dc | ||
|
|
f3895e9dc9 | ||
|
|
3e8deeed07 | ||
|
|
84e91e0c7c | ||
|
|
83e9842dd3 | ||
|
|
3760ff8a7a | ||
|
|
ddecce400a | ||
|
|
6367bd9561 | ||
|
|
104569318e | ||
|
|
4a8465df90 | ||
|
|
78857cd798 | ||
|
|
2c9ee7ae14 | ||
|
|
4a5e0b181f | ||
|
|
35f826c499 | ||
|
|
b565d4f0a8 | ||
|
|
b964b122ed | ||
|
|
cee9f200ad | ||
|
|
214f15e700 | ||
|
|
0341941d3f | ||
|
|
8d1b599d5f | ||
|
|
4022fcb0d9 | ||
|
|
4f14651a42 | ||
|
|
497247d89a | ||
|
|
137d2d4044 | ||
|
|
37eaa07192 | ||
|
|
9d8cb2f57c | ||
|
|
eec327c02b | ||
|
|
093c94aa7c | ||
|
|
19a8ddef86 | ||
|
|
0fdd944418 | ||
|
|
f997393b0e | ||
|
|
0d02d7086c | ||
|
|
913bde0dfe | ||
|
|
21d33cf6b9 | ||
|
|
dc7ac3550e | ||
|
|
e8ba2b1576 | ||
|
|
d9c1ef0926 | ||
|
|
0061bde950 | ||
|
|
5a984de697 | ||
|
|
3603cdf457 | ||
|
|
a74b3e3602 | ||
|
|
8259a748f6 | ||
|
|
59c6e8f233 | ||
|
|
dfa35363b9 | ||
|
|
bab2e86c01 | ||
|
|
fc462d4526 | ||
|
|
5b0486ca26 | ||
|
|
dc4b236951 | ||
|
|
070190d07b | ||
|
|
03550066a7 | ||
|
|
b93cfc5d83 | ||
|
|
ad56177234 | ||
|
|
e22f93f1fb | ||
|
|
56dca02cab | ||
|
|
8795ce975f | ||
|
|
629cdd62f2 | ||
|
|
58f6534d8b | ||
|
|
c7db277aa8 | ||
|
|
4ce4d345e7 | ||
|
|
f714888d48 | ||
|
|
0207b82f82 | ||
|
|
7f1dbeee8b | ||
|
|
a96fa55704 | ||
|
|
4488b27fe3 | ||
|
|
d862a742b0 | ||
|
|
31343b6287 | ||
|
|
e0850ed209 | ||
|
|
52b9f92553 | ||
|
|
cb0addc122 | ||
|
|
5de5a8bfd5 | ||
|
|
396886c6e8 | ||
|
|
3a0db8d550 | ||
|
|
9e661b206c | ||
|
|
14d5c7f3cd | ||
|
|
5291c66ea1 | ||
|
|
ed927a2147 | ||
|
|
2f4a9f283d | ||
|
|
79bfcbf424 | ||
|
|
c9d69d9629 | ||
|
|
6f12029477 | ||
|
|
7bb127de63 | ||
|
|
87de70f4fd | ||
|
|
782fecca46 | ||
|
|
8cd90de70b | ||
|
|
0b9a8ee67c | ||
|
|
652589f636 | ||
|
|
106f7ea112 | ||
|
|
db41b14317 | ||
|
|
609191c3a5 | ||
|
|
7489c9159d | ||
|
|
b48619078d | ||
|
|
523007588d | ||
|
|
b4a6d09d5d | ||
|
|
943aca9739 | ||
|
|
184cabdcb0 | ||
|
|
f8ea431551 | ||
|
|
9732973b73 | ||
|
|
074a7065be | ||
|
|
9f36531f2a | ||
|
|
cfcd21d5c6 | ||
|
|
5c34299b1e | ||
|
|
4fda27fdba | ||
|
|
6a5041042e | ||
|
|
b82aea4a87 | ||
|
|
21bdf6ef14 | ||
|
|
bc1d3ea017 | ||
|
|
1be071683a | ||
|
|
36f22f929d | ||
|
|
1d85da43a6 | ||
|
|
424baed077 | ||
|
|
9508ae5044 | ||
|
|
1697ac0b57 | ||
|
|
3739e2ca5a | ||
|
|
ec9747cc99 | ||
|
|
21f5541158 | ||
|
|
5a82b723c2 | ||
|
|
d7a9c7b161 | ||
|
|
e1d7ec906f | ||
|
|
fe1292e3fa | ||
|
|
61e5c52419 | ||
|
|
d4fae00b80 | ||
|
|
2073e98613 | ||
|
|
ae945b2e6f | ||
|
|
84c0321785 | ||
|
|
f0f8666baa | ||
|
|
5d7b8200fa | ||
|
|
7fd5b2b26a | ||
|
|
85f1efe67e | ||
|
|
d543a0c959 | ||
|
|
ebda396518 | ||
|
|
c086f43964 | ||
|
|
3336b5d55d | ||
|
|
18b007dbf2 | ||
|
|
1efdff0ad1 | ||
|
|
92e315c4e1 | ||
|
|
22e6ea41f7 | ||
|
|
1c81b2c684 | ||
|
|
907358a356 | ||
|
|
9c3011cb9f | ||
|
|
5840a242a6 | ||
|
|
4fd32fc3bd | ||
|
|
2d4419c9d5 | ||
|
|
ee8eb368e7 | ||
|
|
95470b4262 | ||
|
|
bf5b6a540f | ||
|
|
65a27066cc | ||
|
|
f772976676 | ||
|
|
5e6f957e97 | ||
|
|
b23f7a9d91 | ||
|
|
f8d1e5a0d3 | ||
|
|
1ca81887ca | ||
|
|
193fbcba11 | ||
|
|
794fac12a4 | ||
|
|
9b59fb659b | ||
|
|
8d25269de6 | ||
|
|
8314059bf7 | ||
|
|
622052b950 | ||
|
|
49befc1dfd | ||
|
|
3e92ae8bd0 | ||
|
|
ad75754ca6 | ||
|
|
207d2ac63c | ||
|
|
442fede0fe | ||
|
|
584b442824 | ||
|
|
faa9006072 | ||
|
|
01fcd98c84 | ||
|
|
bd7de515b1 | ||
|
|
8d8d47b0d6 | ||
|
|
0cb2c41cba | ||
|
|
23180dad42 | ||
|
|
284e45011e | ||
|
|
ad45063c76 | ||
|
|
c3188ff9a3 | ||
|
|
3d7185fad7 | ||
|
|
fd441e1eff | ||
|
|
0588b68cec | ||
|
|
c2140625f5 | ||
|
|
32029f4dca | ||
|
|
f25aff7b97 | ||
|
|
3825490f0d | ||
|
|
0a1b815546 | ||
|
|
3a94c6c86c | ||
|
|
0d496bb05f | ||
|
|
96a1444e92 | ||
|
|
f9d8f510a0 | ||
|
|
35bc733d28 | ||
|
|
4bef3cc92f | ||
|
|
176a124f1a | ||
|
|
d95306fd60 | ||
|
|
90fa7db13c | ||
|
|
38b223e732 | ||
|
|
4453e447dc | ||
|
|
d378e51492 | ||
|
|
526c1e7c9a | ||
|
|
9bf0d852ee | ||
|
|
fb81202830 | ||
|
|
a1aee44014 | ||
|
|
b57163b7be | ||
|
|
1d52a8fb69 | ||
|
|
a7e8f404f7 | ||
|
|
fa01bdc490 | ||
|
|
62033b5c7a | ||
|
|
ecf9e6e748 | ||
|
|
72e8ce0449 | ||
|
|
801cda3813 | ||
|
|
2b08c5b769 | ||
|
|
e68370359f | ||
|
|
8f23ca5c6b | ||
|
|
e3ccdbb20b | ||
|
|
ab79e5d946 | ||
|
|
b607448fd8 | ||
|
|
a139cd4b5e | ||
|
|
8e91e6ac2c | ||
|
|
9501149bd8 | ||
|
|
4794e7acff | ||
|
|
fd1c213a8d | ||
|
|
36a9f3b3e9 | ||
|
|
6adc8a09c0 | ||
|
|
969c3a2b4d | ||
|
|
2b42848376 | ||
|
|
56f5ec961f | ||
|
|
65efc7e950 | ||
|
|
196c3fc656 | ||
|
|
1759e02c35 | ||
|
|
b83535bc31 | ||
|
|
84cf5ad601 | ||
|
|
3e53660bba | ||
|
|
904cecfa91 | ||
|
|
93c2a67930 | ||
|
|
179444f51e | ||
|
|
32915cf2b7 | ||
|
|
83ab16b161 | ||
|
|
8e16da08d0 | ||
|
|
e8be903ed9 | ||
|
|
c554e2cce7 | ||
|
|
98bd880c73 | ||
|
|
7c2b32fb74 | ||
|
|
b9e6f524e5 | ||
|
|
9fe1e6d0bd | ||
|
|
c7dcbed16f | ||
|
|
20bbfc504f | ||
|
|
3ab6a256e0 | ||
|
|
d9b24a30eb | ||
|
|
7d686abe37 | ||
|
|
2b1fdba7fd | ||
|
|
a46cafe652 | ||
|
|
6b96a26462 | ||
|
|
01ed30ec01 | ||
|
|
34d4c32dfd | ||
|
|
b35f5aca91 | ||
|
|
e101849fb2 | ||
|
|
873e0a4219 | ||
|
|
467fe1d72f | ||
|
|
6b68a50cc4 | ||
|
|
54275dbe38 | ||
|
|
9070c4302d | ||
|
|
47878758fc | ||
|
|
d04c256b73 | ||
|
|
c57ca1ae29 | ||
|
|
0a9f45aacf | ||
|
|
78df52606f | ||
|
|
6fddf4c5aa | ||
|
|
725f9ea012 | ||
|
|
3100099cfa | ||
|
|
9aeb08f968 | ||
|
|
4b6444e93b | ||
|
|
0151733a25 | ||
|
|
4383d29d7b | ||
|
|
9aef3058a6 | ||
|
|
68162f79a1 | ||
|
|
f35fd9842e | ||
|
|
ba63f27e3c | ||
|
|
3488ba05eb | ||
|
|
c20a5b01b4 | ||
|
|
c20c9031f4 | ||
|
|
16cb147d86 | ||
|
|
0e67487508 | ||
|
|
2a8b8aa71d | ||
|
|
3b38c6708c | ||
|
|
72b9684742 | ||
|
|
1d18a3b1a3 | ||
|
|
0bf5d3dae3 | ||
|
|
d9b36ea37c | ||
|
|
2d2ca049fa | ||
|
|
1afb27231c | ||
|
|
a173c77859 | ||
|
|
5d0d0c3102 | ||
|
|
92d58a4e4c | ||
|
|
fee646fbe2 | ||
|
|
c6d82b241e | ||
|
|
e3f7915c38 | ||
|
|
c39993a3ba | ||
|
|
d8212d98ca | ||
|
|
e3ba4320d6 | ||
|
|
81e244be55 | ||
|
|
a195152cc8 | ||
|
|
1322cc1378 | ||
|
|
ee6ef03e24 | ||
|
|
6e10b53e24 | ||
|
|
b5c4f61fef | ||
|
|
9750eba9db | ||
|
|
817bcc78a0 | ||
|
|
69f2d751a2 | ||
|
|
9287e9a2ca | ||
|
|
72e154fbb7 | ||
|
|
d5faae3e7c | ||
|
|
f4a79bb760 | ||
|
|
153df4eca5 | ||
|
|
8f6cd40c7b | ||
|
|
567f7b4d71 | ||
|
|
cad5f39b7f | ||
|
|
1e2845c277 | ||
|
|
f1cdd76fc1 | ||
|
|
715f4025cc | ||
|
|
c894b18165 | ||
|
|
35451fd298 | ||
|
|
43e4c00e1c | ||
|
|
f2af2fe63b | ||
|
|
76fe861281 | ||
|
|
39e2c87955 | ||
|
|
5fce8191f9 | ||
|
|
f1ff5a39ae | ||
|
|
f70ce62f2a | ||
|
|
36af50b2ce | ||
|
|
f80ad4ee58 | ||
|
|
5f41abbdf7 | ||
|
|
3a70b5d7fc | ||
|
|
0a447caa8e | ||
|
|
5cfeb2978b | ||
|
|
00d39eb208 | ||
|
|
7ac546333a | ||
|
|
c1334ea2cb | ||
|
|
89660c9070 | ||
|
|
4d10cb2727 | ||
|
|
8c77ea16cf | ||
|
|
2c59874fd8 | ||
|
|
fdd79c7677 | ||
|
|
db525c2538 | ||
|
|
142de2e2a7 | ||
|
|
facd2027c3 | ||
|
|
90979860cb | ||
|
|
a5c49d1e08 | ||
|
|
243b533150 | ||
|
|
85cb234376 | ||
|
|
4eb9f73a52 | ||
|
|
7a4c8d81e2 | ||
|
|
fc8d451c55 |
25
.github/workflows/patch_faux.yml
vendored
Normal file
25
.github/workflows/patch_faux.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
|
||||
|
||||
name: Skipped Patch Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Patch Test
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
27
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal file
27
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
|
||||
|
||||
name: Skipped Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
container: [1, 2, 3, 4]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
||||
25
CODEOWNERS
25
CODEOWNERS
@@ -3,22 +3,21 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
erpnext/accounts/ @ruthra-kumar
|
||||
erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/subcontracting @mihir-kandoi
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
|
||||
.github/ @deepeshgarg007
|
||||
.github/ @ruthra-kumar
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.69.1"
|
||||
__version__ = "15.73.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -304,7 +304,9 @@ class Account(NestedSet):
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
gl_currency = frappe.db.get_value(
|
||||
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
|
||||
)
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
|
||||
@@ -252,6 +252,10 @@ frappe.treeview_settings["Account"] = {
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
const node = treeview.tree.get_selected_node();
|
||||
if (node.is_root) {
|
||||
frappe.throw(__("Cannot create root account."));
|
||||
}
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
@@ -270,7 +274,8 @@ frappe.treeview_settings["Account"] = {
|
||||
].treeview.page.fields_dict.root_company.get_value() ||
|
||||
frappe.flags.ignore_root_company_validation) &&
|
||||
node.expandable &&
|
||||
!node.hide_add
|
||||
!node.hide_add &&
|
||||
!node.is_root
|
||||
);
|
||||
},
|
||||
click: function () {
|
||||
@@ -286,12 +291,14 @@ frappe.treeview_settings["Account"] = {
|
||||
label: __("View Ledger"),
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
if (node.parent_label) {
|
||||
frappe.route_options["account"] = node.label;
|
||||
}
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
|
||||
@@ -111,17 +111,15 @@ class AccountingDimension(Document):
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
count = 0
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
|
||||
|
||||
for doctype in doclist:
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
"label": doc.label,
|
||||
|
||||
@@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
@@ -88,6 +89,8 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
@@ -552,7 +555,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -609,6 +612,23 @@
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -616,7 +636,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -48,6 +48,7 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
@@ -58,7 +59,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
@@ -152,3 +153,11 @@ class AccountsSettings(Document):
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event"
|
||||
"event",
|
||||
"delinked"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -68,12 +69,20 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
@@ -107,7 +116,8 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -19,9 +21,16 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def on_update(self):
|
||||
if (
|
||||
self.against_voucher_type in ["Purchase Order", "Sales Order"]
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
@@ -109,6 +109,7 @@ class BankAccount(Document):
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"company": self.company,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -377,16 +377,17 @@ def auto_reconcile_vouchers(
|
||||
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,
|
||||
)
|
||||
for bank_transaction_batch in create_batch(bank_transactions, 1000):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transaction_batch,
|
||||
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(
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
@@ -190,6 +192,31 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -462,4 +462,9 @@ def rename_temporarily_named_docs(doctype):
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
|
||||
for hook_type in ("on_gle_rename", "on_sle_rename"):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -163,6 +163,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
|
||||
@@ -191,8 +191,6 @@ class JournalEntry(AccountsController):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -225,8 +223,6 @@ class JournalEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -237,18 +233,6 @@ class JournalEntry(AccountsController):
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
for voucher_no in list(set(order_list)):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.db.get_value(
|
||||
@@ -1044,9 +1028,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_print_format_fields(self):
|
||||
bank_amount = party_amount = total_amount = 0.0
|
||||
currency = (
|
||||
bank_account_currency
|
||||
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
|
||||
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
|
||||
party_type = None
|
||||
for d in self.get("accounts"):
|
||||
if d.party_type in ["Customer", "Supplier"] and d.party:
|
||||
@@ -1096,49 +1078,65 @@ class JournalEntry(AccountsController):
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"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,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"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,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
},
|
||||
row,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -580,6 +580,18 @@ class TestJournalEntry(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pay_to_recd_from(self):
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.pay_to_recd_from = "_Test Receiver"
|
||||
jv.save()
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
|
||||
|
||||
jv.pay_to_recd_from = "_Test Receiver 2"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"reference_name",
|
||||
"reference_due_date",
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -263,12 +265,28 @@
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-05 01:10:50.224840",
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
@@ -279,4 +297,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ class JournalEntryAccount(Document):
|
||||
account: DF.Link
|
||||
account_currency: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
against_account: DF.Text | None
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
@@ -31,7 +32,6 @@ class JournalEntryAccount(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_balance: DF.Currency
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reference_detail_no: DF.Data | None
|
||||
|
||||
@@ -273,6 +273,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
contact_person: function (frm) {
|
||||
|
||||
@@ -117,12 +117,10 @@ class PaymentEntry(AccountsController):
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.update_payment_requests()
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
@@ -222,13 +220,11 @@ class PaymentEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def update_payment_requests(self, cancel=False):
|
||||
@@ -1366,23 +1362,27 @@ class PaymentEntry(AccountsController):
|
||||
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,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# advance reference
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_doctype,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
elif self.book_advance_payments_in_separate_party_account:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
@@ -1487,13 +1487,14 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
if invoice.reconcile_effect_on:
|
||||
posting_date = invoice.reconcile_effect_on
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
posting_date = get_reconciliation_effect_date(
|
||||
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
|
||||
)
|
||||
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)
|
||||
@@ -1511,6 +1512,8 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": invoice.reference_doctype,
|
||||
"against_voucher": invoice.reference_name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
"posting_date": posting_date,
|
||||
}
|
||||
)
|
||||
@@ -1535,6 +1538,8 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
}
|
||||
)
|
||||
gle = self.get_gl_dict(
|
||||
@@ -1683,17 +1688,6 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
self.reference_date = nowdate()
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(pe.paid_to_account_type, "Cash")
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
@@ -84,7 +84,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"exchange_gain_loss",
|
||||
"account",
|
||||
"payment_request",
|
||||
"payment_request_outstanding"
|
||||
"payment_request_outstanding",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -151,12 +153,28 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Reconcile Effect On",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-13 15:56:18.895082",
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
@@ -167,4 +185,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class PaymentEntryReference(Document):
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -26,7 +28,6 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_request: DF.Link | None
|
||||
payment_request_outstanding: DF.Float
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
|
||||
@@ -8,4 +8,14 @@ frappe.ui.form.on("Payment Gateway Account", {
|
||||
frm.set_df_property("payment_gateway", "read_only", 1);
|
||||
}
|
||||
},
|
||||
|
||||
setup(frm) {
|
||||
frm.set_query("payment_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"field_order": [
|
||||
"payment_gateway",
|
||||
"payment_channel",
|
||||
"company",
|
||||
"is_default",
|
||||
"column_break_4",
|
||||
"payment_account",
|
||||
@@ -70,11 +71,21 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-20 13:30:27.722852",
|
||||
"modified": "2025-07-14 16:49:55.210352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
@@ -95,4 +106,4 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentGatewayAccount(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link
|
||||
currency: DF.ReadOnly | None
|
||||
is_default: DF.Check
|
||||
message: DF.SmallText | None
|
||||
@@ -24,7 +25,8 @@ class PaymentGatewayAccount(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
self.name = self.payment_gateway + " - " + self.currency
|
||||
abbr = frappe.db.get_value("Company", self.company, "abbr")
|
||||
self.name = self.payment_gateway + " - " + self.currency + " - " + abbr
|
||||
|
||||
def validate(self):
|
||||
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
|
||||
@@ -34,13 +36,15 @@ class PaymentGatewayAccount(Document):
|
||||
|
||||
def update_default_payment_gateway(self):
|
||||
if self.is_default:
|
||||
frappe.db.sql(
|
||||
"""update `tabPayment Gateway Account` set is_default = 0
|
||||
where is_default = 1 """
|
||||
frappe.db.set_value(
|
||||
"Payment Gateway Account",
|
||||
{"is_default": 1, "name": ["!=", self.name], "company": self.company},
|
||||
"is_default",
|
||||
0,
|
||||
)
|
||||
|
||||
def set_as_default_if_not_set(self):
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
|
||||
if not frappe.db.exists(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company}
|
||||
):
|
||||
self.is_default = 1
|
||||
|
||||
@@ -197,4 +197,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
def test_advance_payment_reconciliation_date_for_older_date(self):
|
||||
old_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"reconciliation_takes_effect_on",
|
||||
"default_advance_paid_account",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||
po = self.create_purchase_order(qty=10, rate=100)
|
||||
|
||||
pay = get_payment_entry(po.doctype, po.name)
|
||||
pay.paid_amount = 1000
|
||||
pay.save().submit()
|
||||
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = 100
|
||||
pr.reconcile()
|
||||
|
||||
pay.reload()
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
|
||||
# test setting of date if not available
|
||||
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
pay.reload()
|
||||
pi1.reload()
|
||||
po.reload()
|
||||
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
pi1.cancel()
|
||||
po.cancel()
|
||||
|
||||
frappe.db.set_value("Company", self.company, old_settings)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
@@ -2145,6 +2206,136 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
|
||||
|
||||
def test_partial_advance_payment_with_closed_fiscal_year(self):
|
||||
"""
|
||||
Test Advance Payment partial reconciliation before period closing and partial after period closing
|
||||
"""
|
||||
default_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"default_advance_paid_account",
|
||||
"reconciliation_takes_effect_on",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)")
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
|
||||
create_fiscal_year(
|
||||
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
# Create advance payment of 1000 (previous FY)
|
||||
pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
# Create purchase invoice of 600 (previous FY)
|
||||
pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True)
|
||||
pi1.posting_date = prev_fy_start_date
|
||||
pi1.set_posting_time = 1
|
||||
pi1.supplier = self.supplier
|
||||
pi1.credit_to = self.creditors
|
||||
pi1.save().submit()
|
||||
|
||||
# Reconcile advance payment
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Verify partial reconciliation
|
||||
pe.reload()
|
||||
pi1.reload()
|
||||
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].allocated_amount, 600)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 400)
|
||||
|
||||
self.assertEqual(pi1.outstanding_amount, 0)
|
||||
self.assertEqual(pi1.status, "Paid")
|
||||
|
||||
# Close accounting period for March (previous FY)
|
||||
pcv = make_period_closing_voucher(
|
||||
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||
)
|
||||
pcv.reload()
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
|
||||
# Change reconciliation setting to "Reconciliation Date"
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
"reconciliation_takes_effect_on",
|
||||
"Reconciliation Date",
|
||||
)
|
||||
|
||||
# Create new purchase invoice for 400 in new fiscal year
|
||||
pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True)
|
||||
pi2.posting_date = today()
|
||||
pi2.set_posting_time = 1
|
||||
pi2.supplier = self.supplier
|
||||
pi2.currency = "INR"
|
||||
pi2.credit_to = self.creditors
|
||||
pi2.save()
|
||||
pi2.submit()
|
||||
|
||||
# Allocate 600 from advance payment to purchase invoice
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pe.reload()
|
||||
pi2.reload()
|
||||
|
||||
# Assert advance payment is fully allocated
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 0)
|
||||
|
||||
# Assert new invoice is fully paid
|
||||
self.assertEqual(pi2.outstanding_amount, 0)
|
||||
self.assertEqual(pi2.status, "Paid")
|
||||
|
||||
# Verify reconciliation dates are correct based on company setting
|
||||
self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date))
|
||||
|
||||
frappe.db.set_value("Company", self.company, default_settings)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -9,6 +9,14 @@ frappe.ui.form.on("Payment Request", {
|
||||
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_gateway_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -539,7 +539,9 @@ def make_payment_request(**args):
|
||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
@@ -782,7 +784,7 @@ def get_gateway_details(args): # nosemgrep
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
"""
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
|
||||
if gateway_account:
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
|
||||
@@ -28,12 +28,14 @@ payment_method = [
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank - _TC",
|
||||
"currency": "INR",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -46,7 +48,11 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
for method in payment_method:
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account",
|
||||
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
|
||||
{
|
||||
"payment_gateway": method["payment_gateway"],
|
||||
"currency": method["currency"],
|
||||
"company": method["company"],
|
||||
},
|
||||
"name",
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
@@ -60,7 +66,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dt="Sales Order",
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Order")
|
||||
@@ -74,7 +80,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dt="Sales Invoice",
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Invoice")
|
||||
@@ -95,7 +101,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -119,7 +125,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -138,7 +144,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -162,7 +168,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -184,7 +190,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -228,7 +234,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -328,7 +334,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
@@ -210,8 +210,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
|
||||
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
|
||||
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -220,14 +222,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"account_currency": frappe.db.get_value(
|
||||
"Account", self.closing_account_head, "account_currency"
|
||||
),
|
||||
"debit_in_account_currency": balance_in_account_currency
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": self.name,
|
||||
|
||||
@@ -14,6 +14,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
}
|
||||
|
||||
company() {
|
||||
erpnext.utils.set_letter_head(this.frm);
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
this.frm.set_value("set_warehouse", "");
|
||||
this.frm.set_value("taxes_and_charges", "");
|
||||
|
||||
@@ -301,6 +301,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -1443,6 +1444,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
@@ -1571,7 +1574,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -1071,10 +1071,3 @@ def create_pos_invoice(**args):
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
return pos_inv
|
||||
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
|
||||
|
||||
@@ -337,6 +337,11 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
invoice.pos_profile = ""
|
||||
|
||||
# Unset Commission Section
|
||||
invoice.set("sales_partner", None)
|
||||
invoice.set("commission_rate", 0)
|
||||
invoice.set("total_commission", 0)
|
||||
|
||||
return invoice
|
||||
|
||||
def get_new_sales_invoice(self):
|
||||
|
||||
@@ -135,6 +135,7 @@ frappe.ui.form.on("POS Profile", {
|
||||
company: function (frm) {
|
||||
frm.trigger("toggle_display_account_head");
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
toggle_display_account_head: function (frm) {
|
||||
|
||||
@@ -92,6 +92,7 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
|
||||
@@ -78,18 +78,18 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Date",
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@@ -330,7 +330,8 @@
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
"label": "Posting Date",
|
||||
"mandatory_depends_on": "eval:(doc.report == 'Accounts Receivable');"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
@@ -400,7 +401,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified": "2025-08-04 18:21:12.603623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import create_batch, getdate
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import DateTimeLikeObject, process_all
|
||||
|
||||
@@ -23,7 +23,23 @@ class ProcessSubscription(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def on_submit(self):
|
||||
process_all(subscription=self.subscription, posting_date=self.posting_date)
|
||||
self.process_all_subscription()
|
||||
|
||||
def process_all_subscription(self):
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
if self.subscription:
|
||||
filters["name"] = self.subscription
|
||||
|
||||
subscriptions = frappe.get_all("Subscription", filters, pluck="name")
|
||||
|
||||
for subscription in create_batch(subscriptions, 500):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
queue="long",
|
||||
subscription=subscription,
|
||||
posting_date=self.posting_date,
|
||||
)
|
||||
|
||||
|
||||
def create_subscription_process(
|
||||
|
||||
@@ -319,6 +319,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -1650,7 +1651,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-09 16:49:22.175081",
|
||||
"modified": "2025-08-04 19:19:11.380664",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -970,7 +970,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
@@ -1223,6 +1223,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
@@ -1307,8 +1310,37 @@ class PurchaseInvoice(BuyingController):
|
||||
net_amt_precision,
|
||||
)
|
||||
|
||||
# Stock ledger value is not matching with the warehouse amount
|
||||
if (
|
||||
if self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
elif (
|
||||
self.update_stock
|
||||
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||
and warehouse_debit_amount
|
||||
@@ -1336,33 +1368,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
|
||||
@@ -5,6 +5,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.desk.form.linked_with import get_child_tables_of_doctypes
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
@@ -169,6 +170,10 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
frappe.db.delete(
|
||||
"Advance Payment Ledger Entry",
|
||||
filters={"voucher_type": doc.doctype, "voucher_no": doc.name},
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
@@ -204,13 +209,29 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
def get_allowed_types_from_settings(child_doc: bool = False):
|
||||
repost_docs = [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
result = repost_docs
|
||||
|
||||
if repost_docs and child_doc:
|
||||
result.extend(get_child_docs(repost_docs))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_child_docs(doc: list) -> list:
|
||||
child_doc = []
|
||||
doc = get_child_tables_of_doctypes(doc)
|
||||
for child_list in doc.values():
|
||||
for child in child_list:
|
||||
if child.get("child_table"):
|
||||
child_doc.append(child["child_table"])
|
||||
return child_doc
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
|
||||
|
||||
|
||||
class RepostAccountingLedgerSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -17,6 +22,24 @@ class RepostAccountingLedgerSettings(Document):
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
allowed_types: DF.Table[RepostAllowedTypes]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
def update_property_for_accounting_dimension(self):
|
||||
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
|
||||
if not doctypes:
|
||||
return
|
||||
doctypes += get_child_docs(doctypes)
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
|
||||
def set_allow_on_submit_for_dimension_fields(doctypes):
|
||||
for dt in doctypes:
|
||||
meta = frappe.get_meta(dt)
|
||||
for dimension in get_accounting_dimensions():
|
||||
df = meta.get_field(dimension)
|
||||
if df and not df.allow_on_submit:
|
||||
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
|
||||
from erpnext.accounts.utils import _delete_adv_pl_entries, _delete_pl_entries, create_payment_ledger_entry
|
||||
|
||||
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
|
||||
@@ -16,6 +16,7 @@ VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal
|
||||
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
|
||||
if voucher_type and voucher_no and gle_map:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||
create_payment_ledger_entry(gle_map, cancel=0)
|
||||
|
||||
|
||||
|
||||
@@ -379,6 +379,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"hide_days": 1,
|
||||
@@ -2189,7 +2190,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified": "2025-08-04 19:20:28.732039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -461,6 +461,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1369,8 +1370,9 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
add_asset_activity(asset.name, _("Asset returned"))
|
||||
asset_status = asset.get_status()
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
if asset.calculate_depreciation and not asset_status == "Fully Depreciated":
|
||||
posting_date = (
|
||||
frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
if self.is_return
|
||||
|
||||
@@ -3055,6 +3055,28 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
# cases where distributed discount amount is not set
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Item",
|
||||
{"name": ["in", [d.name for d in si.items]]},
|
||||
"distributed_discount_amount",
|
||||
0,
|
||||
)
|
||||
|
||||
si.load_from_db()
|
||||
si.additional_discount_account = additional_discount_account
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
expected_gle = [
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 22.0, 0.0, nowdate()],
|
||||
["Service - _TC", 0.0, 100.0, nowdate()],
|
||||
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
|
||||
@@ -756,18 +756,14 @@ def get_prorata_factor(
|
||||
return diff / plan_days
|
||||
|
||||
|
||||
def process_all(subscription: str | None = None, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
"""
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
if subscription:
|
||||
filters["name"] = subscription
|
||||
|
||||
for subscription in frappe.get_all("Subscription", filters, pluck="name"):
|
||||
for subscription_name in subscription:
|
||||
try:
|
||||
subscription = frappe.get_doc("Subscription", subscription)
|
||||
subscription = frappe.get_doc("Subscription", subscription_name)
|
||||
subscription.process(posting_date)
|
||||
frappe.db.commit()
|
||||
except frappe.ValidationError:
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount",
|
||||
@@ -102,10 +102,11 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-27 21:47:34.396071",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -148,4 +149,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
@@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
@@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
pe.cancel()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
@@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
self.assertEqual(so2.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
@@ -463,8 +465,77 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.allocations = [x for x in unreconcile.allocations if x.reference_name == si.name]
|
||||
unreconcile.save().submit()
|
||||
|
||||
# after unreconcilaition advance paid will be reduced
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_unreconcile_advance_from_journal_entry(self):
|
||||
po = create_purchase_order(
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
item=self.item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
transaction_date=today(),
|
||||
do_not_submit=False,
|
||||
)
|
||||
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": po.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Creditors - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": po.supplier,
|
||||
"debit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": po.doctype,
|
||||
"reference_name": po.name,
|
||||
},
|
||||
{"account": "Cash - _TC", "credit_in_account_currency": 100},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": je.doctype,
|
||||
"voucher_no": je.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 1)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEqual([po.name], allocations)
|
||||
unreconcile.save().submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
|
||||
@@ -12,7 +12,6 @@ from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_advance_payment_doctypes,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -45,31 +44,12 @@ class UnreconcilePayment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_allocations_from_payment(self):
|
||||
allocated_references = []
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
allocated_references = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(
|
||||
(ple.docstatus == 1)
|
||||
& (ple.voucher_type == self.voucher_type)
|
||||
& (ple.voucher_no == self.voucher_no)
|
||||
& (ple.voucher_no != ple.against_voucher_no)
|
||||
)
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
return get_linked_payments_for_doc(
|
||||
company=self.company,
|
||||
doctype=self.voucher_type,
|
||||
docname=self.voucher_no,
|
||||
)
|
||||
|
||||
return allocated_references
|
||||
|
||||
def add_references(self):
|
||||
allocations = self.get_allocations_from_payment()
|
||||
|
||||
@@ -82,27 +62,43 @@ class UnreconcilePayment(Document):
|
||||
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
|
||||
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
|
||||
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
|
||||
|
||||
# update outstanding amounts
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
alloc.reference_doctype,
|
||||
alloc.reference_name,
|
||||
alloc.account,
|
||||
alloc.party_type,
|
||||
alloc.party,
|
||||
)
|
||||
if doc.doctype in get_advance_payment_doctypes():
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def doc_has_references(doctype: str | None = None, docname: str | None = None):
|
||||
count = 0
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return frappe.db.count(
|
||||
count = frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
|
||||
)
|
||||
else:
|
||||
return frappe.db.count(
|
||||
count = frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
|
||||
)
|
||||
count += frappe.db.count(
|
||||
"Advance Payment Ledger Entry",
|
||||
filters={
|
||||
"delinked": 0,
|
||||
"voucher_no": docname,
|
||||
"voucher_type": doctype,
|
||||
"event": ["=", "Submit"],
|
||||
},
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -124,9 +120,12 @@ def get_linked_payments_for_doc(
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.company,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.voucher_type.as_("reference_doctype"),
|
||||
ple.voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
@@ -148,19 +147,52 @@ def get_linked_payments_for_doc(
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
)
|
||||
|
||||
res = query.run(as_dict=True)
|
||||
|
||||
res += get_linked_advances(company, _dn)
|
||||
|
||||
return res
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_linked_advances(company, docname):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
criteria = [
|
||||
(adv.company == company),
|
||||
(adv.delinked == 0),
|
||||
(adv.voucher_no == docname),
|
||||
(adv.event == "Submit"),
|
||||
]
|
||||
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(
|
||||
adv.company,
|
||||
adv.against_voucher_type.as_("reference_doctype"),
|
||||
adv.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(adv.amount)).as_("allocated_amount"),
|
||||
adv.currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.groupby(adv.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
|
||||
@@ -186,6 +186,15 @@ def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
|
||||
round_off_account, default_currency = frappe.get_cached_value(
|
||||
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
|
||||
)
|
||||
if not precision:
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=default_currency,
|
||||
)
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
@@ -203,6 +212,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
if d.account == round_off_account:
|
||||
d.cost_center = cost_center_allocation[0][0]
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
@@ -305,6 +319,8 @@ def get_merge_properties(dimensions=None):
|
||||
"project",
|
||||
"finance_book",
|
||||
"voucher_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
]
|
||||
if dimensions:
|
||||
merge_properties.extend(dimensions)
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
@@ -127,6 +128,8 @@ class ReceivablePayableReport:
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
@@ -134,8 +137,7 @@ class ReceivablePayableReport:
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
query, param = self.ple_query
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -148,9 +150,8 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
@@ -318,6 +319,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -861,18 +935,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
query, param = query.walk()
|
||||
|
||||
match_conditions = build_match_conditions("Payment Ledger Entry")
|
||||
if match_conditions:
|
||||
query += " AND " + match_conditions
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_query = (query, param)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@@ -1253,3 +1324,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -14,9 +14,16 @@ erpnext.utils.add_dimensions("Cash Flow", 10);
|
||||
|
||||
frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
|
||||
|
||||
frappe.query_reports["Cash Flow"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Cash Flow"]["filters"].push(
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_opening_and_closing_balance",
|
||||
label: __("Show Opening and Closing Balance"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cstr, flt
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
@@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
get_period_list,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
|
||||
get_net_profit_loss,
|
||||
@@ -119,10 +124,20 @@ def execute(filters=None):
|
||||
filters,
|
||||
)
|
||||
|
||||
add_total_row_account(
|
||||
net_change_in_cash = add_total_row_account(
|
||||
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
|
||||
|
||||
if filters.show_opening_and_closing_balance:
|
||||
show_opening_and_closing_balance(data, period_list, company_currency, net_change_in_cash, filters)
|
||||
|
||||
columns = get_columns(
|
||||
filters.periodicity,
|
||||
period_list,
|
||||
filters.accumulated_values,
|
||||
filters.company,
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
@@ -255,6 +270,137 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
|
||||
out.append(total_row)
|
||||
out.append({})
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def show_opening_and_closing_balance(out, period_list, currency, net_change_in_cash, filters):
|
||||
opening_balance = {
|
||||
"section_name": "Opening",
|
||||
"section": "Opening",
|
||||
"currency": currency,
|
||||
}
|
||||
closing_balance = {
|
||||
"section_name": "Closing (Opening + Total)",
|
||||
"section": "Closing (Opening + Total)",
|
||||
"currency": currency,
|
||||
}
|
||||
|
||||
opening_amount = get_opening_balance(filters.company, period_list, filters) or 0.0
|
||||
running_total = opening_amount
|
||||
|
||||
for i, period in enumerate(period_list):
|
||||
key = period["key"]
|
||||
change = net_change_in_cash.get(key, 0.0)
|
||||
|
||||
opening_balance[key] = opening_amount if i == 0 else running_total
|
||||
running_total += change
|
||||
closing_balance[key] = running_total
|
||||
|
||||
opening_balance["total"] = opening_balance[period_list[0]["key"]]
|
||||
closing_balance["total"] = closing_balance[period_list[-1]["key"]]
|
||||
|
||||
out.extend([opening_balance, net_change_in_cash, closing_balance, {}])
|
||||
|
||||
|
||||
def get_opening_balance(company, period_list, filters):
|
||||
from copy import deepcopy
|
||||
|
||||
cash_value = {}
|
||||
account_types = get_cash_flow_accounts()
|
||||
net_profit_loss = 0.0
|
||||
|
||||
local_filters = deepcopy(filters)
|
||||
local_filters.start_date, local_filters.end_date = get_opening_range_using_fiscal_year(
|
||||
company, period_list
|
||||
)
|
||||
|
||||
for section in account_types:
|
||||
section_name = section.get("section_name")
|
||||
cash_value.setdefault(section_name, 0.0)
|
||||
|
||||
if section_name == "Operations":
|
||||
net_profit_loss += get_net_income(company, period_list, local_filters)
|
||||
|
||||
for account in section.get("account_types", []):
|
||||
account_type = account.get("account_type")
|
||||
local_filters.account_type = account_type
|
||||
|
||||
amount = get_account_type_based_gl_data(company, local_filters) or 0.0
|
||||
|
||||
if account_type == "Depreciation":
|
||||
cash_value[section_name] += amount * -1
|
||||
else:
|
||||
cash_value[section_name] += amount
|
||||
|
||||
return sum(cash_value.values()) + net_profit_loss
|
||||
|
||||
|
||||
def get_net_income(company, period_list, filters):
|
||||
gl_entries_by_account_for_income, gl_entries_by_account_for_expense = {}, {}
|
||||
income, expense = 0.0, 0.0
|
||||
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
|
||||
|
||||
for root_type in ["Income", "Expense"]:
|
||||
for root in frappe.db.sql(
|
||||
"""select lft, rgt from tabAccount
|
||||
where root_type=%s and ifnull(parent_account, '') = ''""",
|
||||
root_type,
|
||||
as_dict=1,
|
||||
):
|
||||
set_gl_entries_by_account(
|
||||
company,
|
||||
from_date,
|
||||
to_date,
|
||||
filters,
|
||||
gl_entries_by_account_for_income
|
||||
if root_type == "Income"
|
||||
else gl_entries_by_account_for_expense,
|
||||
root.lft,
|
||||
root.rgt,
|
||||
root_type=root_type,
|
||||
ignore_closing_entries=True,
|
||||
)
|
||||
|
||||
for entries in gl_entries_by_account_for_income.values():
|
||||
for entry in entries:
|
||||
if entry.posting_date <= to_date:
|
||||
amount = (entry.debit - entry.credit) * -1
|
||||
income = flt((income + amount), 2)
|
||||
|
||||
for entries in gl_entries_by_account_for_expense.values():
|
||||
for entry in entries:
|
||||
if entry.posting_date <= to_date:
|
||||
amount = entry.debit - entry.credit
|
||||
expense = flt((expense + amount), 2)
|
||||
|
||||
return income - expense
|
||||
|
||||
|
||||
def get_opening_range_using_fiscal_year(company, period_list):
|
||||
first_from_date = period_list[0]["from_date"]
|
||||
previous_day = first_from_date - timedelta(days=1)
|
||||
|
||||
# Get the earliest fiscal year for the company
|
||||
|
||||
FiscalYear = DocType("Fiscal Year")
|
||||
FiscalYearCompany = DocType("Fiscal Year Company")
|
||||
|
||||
earliest_fy = (
|
||||
frappe.qb.from_(FiscalYear)
|
||||
.join(FiscalYearCompany)
|
||||
.on(FiscalYearCompany.parent == FiscalYear.name)
|
||||
.select(FiscalYear.year_start_date)
|
||||
.where(FiscalYearCompany.company == company)
|
||||
.orderby(FiscalYear.year_start_date, order=Order.asc)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
if not earliest_fy:
|
||||
frappe.throw(_("Not able to find the earliest Fiscal Year for the given company."))
|
||||
|
||||
company_start_date = earliest_fy[0]["year_start_date"]
|
||||
return company_start_date, previous_day
|
||||
|
||||
|
||||
def get_report_summary(summary_data, currency):
|
||||
report_summary = []
|
||||
@@ -276,7 +422,7 @@ def get_chart_data(columns, data, currency):
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
]
|
||||
datasets = datasets[:-1]
|
||||
datasets = datasets[:-2]
|
||||
|
||||
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}
|
||||
|
||||
|
||||
@@ -275,12 +275,25 @@ class PartyLedgerSummaryReport:
|
||||
if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes":
|
||||
self.party_data[gle.party].opening_balance += amount
|
||||
else:
|
||||
if amount > 0:
|
||||
self.party_data[gle.party].invoiced_amount += amount
|
||||
elif gle.voucher_no in self.return_invoices:
|
||||
self.party_data[gle.party].return_amount -= amount
|
||||
# Cache the party data reference to avoid repeated dictionary lookups
|
||||
party_data = self.party_data[gle.party]
|
||||
|
||||
# Check if this is a direct return invoice (most specific condition first)
|
||||
if gle.voucher_no in self.return_invoices:
|
||||
party_data.return_amount -= amount
|
||||
# Check if this entry is against a return invoice
|
||||
elif gle.against_voucher in self.return_invoices:
|
||||
# For entries against return invoices, positive amounts are payments
|
||||
if amount > 0:
|
||||
party_data.paid_amount -= amount
|
||||
else:
|
||||
party_data.invoiced_amount += amount
|
||||
# Normal transaction logic
|
||||
else:
|
||||
self.party_data[gle.party].paid_amount -= amount
|
||||
if amount > 0:
|
||||
party_data.invoiced_amount += amount
|
||||
else:
|
||||
party_data.paid_amount -= amount
|
||||
|
||||
out = []
|
||||
for party, row in self.party_data.items():
|
||||
@@ -289,7 +302,7 @@ class PartyLedgerSummaryReport:
|
||||
or row.invoiced_amount
|
||||
or row.paid_amount
|
||||
or row.return_amount
|
||||
or row.closing_amount
|
||||
or row.closing_balance # Fixed typo from closing_amount to closing_balance
|
||||
):
|
||||
total_party_adjustment = sum(
|
||||
amount for amount in self.party_adjustment_details.get(party, {}).values()
|
||||
@@ -313,6 +326,7 @@ class PartyLedgerSummaryReport:
|
||||
gle.party,
|
||||
gle.voucher_type,
|
||||
gle.voucher_no,
|
||||
gle.against_voucher, # For handling returned invoices (Credit/Debit Notes)
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.is_opening,
|
||||
|
||||
@@ -150,3 +150,157 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin):
|
||||
for field in expected_after_cr_and_payment:
|
||||
with self.subTest(field=field):
|
||||
self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field))
|
||||
|
||||
def test_journal_voucher_against_return_invoice(self):
|
||||
filters = {"company": self.company, "from_date": today(), "to_date": today()}
|
||||
|
||||
# Create Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.save().submit()
|
||||
|
||||
expected = {
|
||||
"party": "_Test Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 1000.0,
|
||||
"paid_amount": 0,
|
||||
"return_amount": 0,
|
||||
"closing_balance": 1000.0,
|
||||
"currency": "INR",
|
||||
"customer_name": "_Test Customer",
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
for field in expected:
|
||||
with self.subTest(field=field):
|
||||
actual_value = report[0].get(field)
|
||||
expected_value = expected.get(field)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
|
||||
# Create Payment Entry (Receive) for the first invoice
|
||||
pe1 = self.create_payment_entry(si1.name, True)
|
||||
pe1.paid_amount = 1000 # Full payment 1000.0
|
||||
pe1.save().submit()
|
||||
|
||||
expected_after_payment = {
|
||||
"party": "_Test Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 1000.0,
|
||||
"paid_amount": 1000.0,
|
||||
"return_amount": 0,
|
||||
"closing_balance": 0.0,
|
||||
"currency": "INR",
|
||||
"customer_name": "_Test Customer",
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
for field in expected_after_payment:
|
||||
with self.subTest(field=field):
|
||||
actual_value = report[0].get(field)
|
||||
expected_value = expected_after_payment.get(field)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
|
||||
# Create Credit Note (return invoice) for first invoice (1000.0)
|
||||
cr_note = self.create_credit_note(si1.name, do_not_submit=True)
|
||||
cr_note.items[0].qty = -10 # 1 item of qty 10 at rate 100 (Amount: 1000.0)
|
||||
cr_note.save().submit()
|
||||
|
||||
expected_after_cr_note = {
|
||||
"party": "_Test Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 1000.0,
|
||||
"paid_amount": 1000.0,
|
||||
"return_amount": 1000.0,
|
||||
"closing_balance": -1000.0,
|
||||
"currency": "INR",
|
||||
"customer_name": "_Test Customer",
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
for field in expected_after_cr_note:
|
||||
with self.subTest(field=field):
|
||||
actual_value = report[0].get(field)
|
||||
expected_value = expected_after_cr_note.get(field)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
|
||||
# Create Payment Entry for the returned amount (1000.0) - Pay the customer back
|
||||
pe2 = get_payment_entry("Sales Invoice", cr_note.name, bank_account=self.cash)
|
||||
pe2.insert().submit()
|
||||
|
||||
expected_after_cr_and_return_payment = {
|
||||
"party": "_Test Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 1000.0,
|
||||
"paid_amount": 0,
|
||||
"return_amount": 1000.0,
|
||||
"closing_balance": 0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
for field in expected_after_cr_and_return_payment:
|
||||
with self.subTest(field=field):
|
||||
actual_value = report[0].get(field)
|
||||
expected_value = expected_after_cr_and_return_payment.get(field)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
|
||||
# Create second Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.save().submit()
|
||||
|
||||
# Create Payment Entry (Receive) for the second invoice - payment (500.0)
|
||||
pe3 = self.create_payment_entry(si2.name, True)
|
||||
pe3.paid_amount = 500 # Partial payment 500.0
|
||||
pe3.save().submit()
|
||||
|
||||
expected_after_cr_and_payment = {
|
||||
"party": "_Test Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0.0,
|
||||
"invoiced_amount": 2000.0,
|
||||
"paid_amount": 500.0,
|
||||
"return_amount": 1000.0,
|
||||
"closing_balance": 500.0,
|
||||
"currency": "INR",
|
||||
"customer_name": "_Test Customer",
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
for field in expected_after_cr_and_payment:
|
||||
with self.subTest(field=field):
|
||||
actual_value = report[0].get(field)
|
||||
expected_value = expected_after_cr_and_payment.get(field)
|
||||
self.assertEqual(
|
||||
actual_value,
|
||||
expected_value,
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
|
||||
gl_filters["dimensions"] = set(dimension_list)
|
||||
gl_filters["dimensions"] = tuple(set(dimension_list))
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
@@ -197,6 +197,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Show Net Values in Party Account"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_amount_in_company_currency",
|
||||
label: __("Show Credit / Debit in Company Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "add_values_in_transaction_currency",
|
||||
label: __("Add Columns in Transaction Currency"),
|
||||
|
||||
@@ -204,7 +204,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map)
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
else:
|
||||
return gl_entries
|
||||
|
||||
@@ -605,6 +605,18 @@ def get_columns(filters):
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
company_currency = get_company_currency(filters.get("company") or get_default_company())
|
||||
|
||||
if (
|
||||
filters.get("show_amount_in_company_currency")
|
||||
and filters["presentation_currency"] != company_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
f'Presentation Currency cannot be {frappe.bold(filters["presentation_currency"])} , When {frappe.bold("Show Credit / Debit in Company Currency")} is enabled.'
|
||||
)
|
||||
)
|
||||
|
||||
columns = [
|
||||
{
|
||||
"label": _("GL Entry"),
|
||||
|
||||
@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.unrealized_profit_loss_account,
|
||||
pii.item_code,
|
||||
pii.description,
|
||||
pii.item_name,
|
||||
pii.item_group,
|
||||
pii.item_name.as_("pi_item_name"),
|
||||
pii.item_group.as_("pi_item_group"),
|
||||
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
query = apply_order_by_conditions(doctype, query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -343,7 +343,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
||||
for opts in ("company", "customer"):
|
||||
if filters.get(opts):
|
||||
query = query.where(si[opts] == filters[opts])
|
||||
@@ -355,10 +355,7 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
sales_invoice = frappe.db.get_all(
|
||||
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||
)
|
||||
query = query.where(si.name.isin(sales_invoice))
|
||||
query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
@@ -397,15 +394,18 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
return query
|
||||
|
||||
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
def apply_order_by_conditions(doctype, query, filters):
|
||||
invoice = f"`tab{doctype}`"
|
||||
invoice_item = f"`tab{doctype} Item`"
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
query += f" order by {invoice_item}.parent desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query += f" order by {ii.item_code}"
|
||||
query += f" order by {invoice_item}.item_code"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query += f" order by {ii.item_group}"
|
||||
query += f" order by {invoice_item}.item_group"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
@@ -415,14 +415,17 @@ def apply_order_by_conditions(query, si, ii, filters):
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.join(sii)
|
||||
.on(si.name == sii.parent)
|
||||
.left_join(sip)
|
||||
.on(sip.parent == si.name)
|
||||
.left_join(item)
|
||||
.on(sii.item_code == item.name)
|
||||
.select(
|
||||
@@ -462,6 +465,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
sii.qty,
|
||||
sip.mode_of_payment,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
@@ -481,17 +485,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
if filters.get("customer_group"):
|
||||
query = query.where(si.customer_group == filters["customer_group"])
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
query = apply_conditions(query, si, sii, sip, filters, additional_conditions)
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
query = apply_order_by_conditions(doctype, query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
@@ -763,25 +767,13 @@ def add_total_row(
|
||||
def get_display_value(filters, group_by_field, item):
|
||||
if filters.get("group_by") == "Item":
|
||||
if item.get("item_code") != item.get("item_name"):
|
||||
value = (
|
||||
cstr(item.get("item_code"))
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ cstr(item.get("item_name"))
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get('item_code')}: {item.get('item_name')}"
|
||||
else:
|
||||
value = item.get("item_code", "")
|
||||
elif filters.get("group_by") in ("Customer", "Supplier"):
|
||||
party = frappe.scrub(filters.get("group_by"))
|
||||
if item.get(party) != item.get(party + "_name"):
|
||||
value = (
|
||||
item.get(party)
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ item.get(party + "_name")
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get(party)}: {item.get(party + '_name')}"
|
||||
else:
|
||||
value = item.get(party)
|
||||
else:
|
||||
|
||||
@@ -46,6 +46,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
child_doctype.item_name,
|
||||
child_doctype.description,
|
||||
project_field,
|
||||
doctype.company,
|
||||
)
|
||||
.where(
|
||||
(doctype.docstatus == 1)
|
||||
|
||||
@@ -46,6 +46,7 @@ class PaymentLedger:
|
||||
against_voucher_no=ple.against_voucher_no,
|
||||
amount=ple.amount,
|
||||
currency=ple.account_currency,
|
||||
company=ple.company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -77,6 +78,7 @@ class PaymentLedger:
|
||||
against_voucher_no="Outstanding:",
|
||||
amount=total,
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -85,7 +87,12 @@ class PaymentLedger:
|
||||
voucher_data.append(entry)
|
||||
|
||||
# empty row
|
||||
voucher_data.append(frappe._dict())
|
||||
voucher_data.append(
|
||||
frappe._dict(
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
)
|
||||
self.data.extend(voucher_data)
|
||||
|
||||
def build_conditions(self):
|
||||
@@ -130,7 +137,6 @@ class PaymentLedger:
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
@@ -195,7 +201,7 @@ class PaymentLedger:
|
||||
label=_("Amount"),
|
||||
fieldname="amount",
|
||||
fieldtype="Currency",
|
||||
options=company_currency,
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-05-06 12:28:23",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"idx": 6,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-10-06 06:26:07.881340",
|
||||
"letterhead": null,
|
||||
"modified": "2025-07-17 23:16:19.892044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Partners Commission",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
|
||||
"query": "SELECT\n sales_partner as \"Sales Partner:Link / Sales Partner:220\",\n sum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n sum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n sum(total_commission) as \"Total Commission:Currency:170\",\n sum(total_commission)*100 / sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n (\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabSales Invoice` \n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n\n UNION ALL\n\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabPOS Invoice`\n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n ) AS sub\nGROUP BY\n sales_partner\nORDER BY\n \"Total Commission:Currency:120\"",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
@@ -26,5 +27,6 @@
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
|
||||
out = []
|
||||
entries = {}
|
||||
for name, details in gle_map.items():
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
@@ -119,8 +120,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
key = entry.voucher_no
|
||||
if key in entries:
|
||||
entries[key]["tax_amount"] += tax_amount
|
||||
else:
|
||||
entries[key] = row
|
||||
out = list(entries.values())
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
|
||||
return rate
|
||||
|
||||
|
||||
def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
|
||||
"""
|
||||
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
|
||||
in `currency_info`.
|
||||
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
company_currency = currency_info["company_currency"]
|
||||
|
||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||
exchange_gain_or_loss = False
|
||||
|
||||
if filters and isinstance(filters.get("account"), list):
|
||||
account_filter = filters.get("account")
|
||||
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
|
||||
|
||||
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
|
||||
|
||||
for entry in gl_entries:
|
||||
debit = flt(entry["debit"])
|
||||
@@ -107,7 +114,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and not exchange_gain_or_loss
|
||||
) and not filters.get("show_amount_in_company_currency"):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from json import loads
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
|
||||
from frappe.query_builder.functions import Count, Max, Sum
|
||||
@@ -26,6 +28,7 @@ from frappe.utils import (
|
||||
nowdate,
|
||||
)
|
||||
from pypika import Order
|
||||
from pypika.functions import Coalesce
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
import erpnext
|
||||
@@ -469,63 +472,45 @@ def reconcile_against_document(
|
||||
reconciled_entries[(row.voucher_type, row.voucher_no)].append(row)
|
||||
|
||||
for key, entries in reconciled_entries.items():
|
||||
voucher_type = key[0]
|
||||
voucher_no = key[1]
|
||||
voucher_type, voucher_no = key
|
||||
|
||||
# cancel advance entry
|
||||
doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
frappe.flags.ignore_party_validation = True
|
||||
|
||||
# When Advance is allocated from an Order to an Invoice
|
||||
# whole ledger must be reposted
|
||||
repost_whole_ledger = any([x.voucher_detail_no for x in entries])
|
||||
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
|
||||
if repost_whole_ledger:
|
||||
doc.make_gl_entries(cancel=1)
|
||||
else:
|
||||
doc.make_advance_gl_entries(cancel=1)
|
||||
else:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
|
||||
reposting_rows = []
|
||||
for entry in entries:
|
||||
check_if_advance_entry_modified(entry)
|
||||
validate_allocated_amount(entry)
|
||||
|
||||
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
|
||||
|
||||
# update ref in advance entry
|
||||
if voucher_type == "Journal Entry":
|
||||
referenced_row, update_advance_paid = update_reference_in_journal_entry(
|
||||
entry, doc, do_not_save=False
|
||||
)
|
||||
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
|
||||
# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
|
||||
# amount and account in args
|
||||
# referenced_row is used to deduplicate gain/loss journal
|
||||
entry.update({"referenced_row": referenced_row})
|
||||
entry.update({"referenced_row": referenced_row.name})
|
||||
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
|
||||
else:
|
||||
referenced_row, update_advance_paid = update_reference_in_payment_entry(
|
||||
referenced_row = update_reference_in_payment_entry(
|
||||
entry,
|
||||
doc,
|
||||
do_not_save=True,
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
|
||||
reposting_rows.append(referenced_row)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
|
||||
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
|
||||
# When Advance is allocated from an Order to an Invoice
|
||||
# whole ledger must be reposted
|
||||
if repost_whole_ledger:
|
||||
doc.make_gl_entries()
|
||||
else:
|
||||
# both ledgers must be posted to for `Advance` in separate account feature
|
||||
# TODO: find a more efficient way post only for the new linked vouchers
|
||||
doc.make_advance_gl_entries()
|
||||
for row in reposting_rows:
|
||||
doc.make_advance_gl_entries(entry=row)
|
||||
else:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||
@@ -542,11 +527,6 @@ def reconcile_against_document(
|
||||
entry.party_type,
|
||||
entry.party,
|
||||
)
|
||||
# update advance paid in Advance Receivable/Payable doctypes
|
||||
if update_advance_paid:
|
||||
for t, n in update_advance_paid:
|
||||
frappe.get_doc(t, n).set_total_advance_paid()
|
||||
|
||||
frappe.flags.ignore_party_validation = False
|
||||
|
||||
|
||||
@@ -627,12 +607,6 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
"""
|
||||
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
update_advance_paid = []
|
||||
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
rev_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d["dr_or_cr"] == "credit_in_account_currency"
|
||||
@@ -685,6 +659,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
new_row.is_advance = cstr(jv_detail.is_advance)
|
||||
new_row.docstatus = 1
|
||||
|
||||
if jv_detail.get("reference_type") in get_advance_payment_doctypes():
|
||||
new_row.advance_voucher_type = jv_detail.get("reference_type")
|
||||
new_row.advance_voucher_no = jv_detail.get("reference_name")
|
||||
|
||||
# will work as update after submit
|
||||
journal_entry.flags.ignore_validate_update_after_submit = True
|
||||
# Ledgers will be reposted by Reconciliation tool
|
||||
@@ -692,7 +670,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
if not do_not_save:
|
||||
journal_entry.save(ignore_permissions=True)
|
||||
|
||||
return new_row.name, update_advance_paid
|
||||
return new_row
|
||||
|
||||
|
||||
def update_reference_in_payment_entry(
|
||||
@@ -711,20 +689,19 @@ def update_reference_in_payment_entry(
|
||||
"account": d.account,
|
||||
"dimensions": d.dimensions,
|
||||
}
|
||||
update_advance_paid = []
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
|
||||
reconcile_on = get_reconciliation_effect_date(
|
||||
d.against_voucher_type, d.against_voucher, payment_entry.company, payment_entry.posting_date
|
||||
)
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they are getting unlinked
|
||||
if existing_row.get("reference_doctype") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((existing_row.reference_doctype, existing_row.reference_name))
|
||||
|
||||
if d.allocated_amount <= existing_row.allocated_amount:
|
||||
existing_row.allocated_amount -= d.allocated_amount
|
||||
|
||||
@@ -732,7 +709,13 @@ def update_reference_in_payment_entry(
|
||||
new_row.docstatus = 1
|
||||
for field in list(reference_details):
|
||||
new_row.set(field, reference_details[field])
|
||||
|
||||
if existing_row.reference_doctype in advance_payment_doctypes:
|
||||
new_row.advance_voucher_type = existing_row.reference_doctype
|
||||
new_row.advance_voucher_no = existing_row.reference_name
|
||||
|
||||
row = new_row
|
||||
|
||||
else:
|
||||
new_row = payment_entry.append("references")
|
||||
new_row.docstatus = 1
|
||||
@@ -766,23 +749,25 @@ def update_reference_in_payment_entry(
|
||||
payment_entry.flags.ignore_reposting_on_reconciliation = True
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
return row, update_advance_paid
|
||||
|
||||
return row
|
||||
|
||||
|
||||
def get_reconciliation_effect_date(reference, company, posting_date):
|
||||
def get_reconciliation_effect_date(against_voucher_type, against_voucher, company, posting_date):
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
|
||||
# default
|
||||
reconcile_on = posting_date
|
||||
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
if against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(
|
||||
reference.against_voucher_type, reference.against_voucher, date_field
|
||||
)
|
||||
reconcile_on = frappe.db.get_value(against_voucher_type, against_voucher, date_field)
|
||||
if getdate(reconcile_on) < getdate(posting_date):
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
@@ -943,6 +928,24 @@ def update_accounting_ledgers_after_reference_removal(
|
||||
ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
|
||||
ple_update_query.run()
|
||||
|
||||
# Advance Payment
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
adv_ple = (
|
||||
qb.update(adv)
|
||||
.set(adv.delinked, 1)
|
||||
.set(adv.modified, now())
|
||||
.set(adv.modified_by, frappe.session.user)
|
||||
.where(adv.delinked == 0)
|
||||
.where(
|
||||
((adv.against_voucher_type == ref_type) & (adv.against_voucher_no == ref_no))
|
||||
| ((adv.voucher_type == ref_type) & (adv.voucher_no == ref_no))
|
||||
)
|
||||
)
|
||||
if payment_name:
|
||||
adv_ple = adv_ple.where(adv.voucher_no == payment_name)
|
||||
|
||||
adv_ple.run()
|
||||
|
||||
|
||||
def remove_ref_from_advance_section(ref_doc: object = None):
|
||||
# TODO: this might need some testing
|
||||
@@ -979,6 +982,8 @@ def remove_ref_doc_link_from_jv(
|
||||
qb.update(jea)
|
||||
.set(jea.reference_type, None)
|
||||
.set(jea.reference_name, None)
|
||||
.set(jea.advance_voucher_type, None)
|
||||
.set(jea.advance_voucher_no, None)
|
||||
.set(jea.modified, now())
|
||||
.set(jea.modified_by, frappe.session.user)
|
||||
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
|
||||
@@ -1335,6 +1340,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||
"payment_account": bank_account.name,
|
||||
"currency": bank_account.account_currency,
|
||||
"payment_channel": payment_channel,
|
||||
"company": company,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
@@ -1489,6 +1495,11 @@ def _delete_pl_entries(voucher_type, voucher_no):
|
||||
qb.from_(ple).delete().where((ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)).run()
|
||||
|
||||
|
||||
def _delete_adv_pl_entries(voucher_type, voucher_no):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where((adv.voucher_type == voucher_type) & (adv.voucher_no == voucher_no)).run()
|
||||
|
||||
|
||||
def _delete_gl_entries(voucher_type, voucher_no):
|
||||
gle = qb.DocType("GL Entry")
|
||||
qb.from_(gle).delete().where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)).run()
|
||||
@@ -1808,6 +1819,11 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
dr_or_cr *= -1
|
||||
dr_or_cr_account_currency *= -1
|
||||
|
||||
against_voucher_type = (
|
||||
gle.against_voucher_type if gle.against_voucher_type else gle.voucher_type
|
||||
)
|
||||
against_voucher_no = gle.against_voucher if gle.against_voucher else gle.voucher_no
|
||||
|
||||
ple = frappe._dict(
|
||||
doctype="Payment Ledger Entry",
|
||||
posting_date=gle.posting_date,
|
||||
@@ -1822,14 +1838,12 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
voucher_detail_no=gle.voucher_detail_no,
|
||||
against_voucher_type=gle.against_voucher_type
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
against_voucher_type=against_voucher_type,
|
||||
against_voucher_no=against_voucher_no,
|
||||
account_currency=gle.account_currency,
|
||||
amount=dr_or_cr,
|
||||
amount_in_account_currency=dr_or_cr_account_currency,
|
||||
delinked=True if cancel else False,
|
||||
delinked=cancel,
|
||||
remarks=gle.remarks,
|
||||
)
|
||||
|
||||
@@ -1838,10 +1852,40 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
ple[dimension.fieldname] = gle.get(dimension.fieldname)
|
||||
|
||||
if gle.advance_voucher_no:
|
||||
# create advance entry
|
||||
adv = get_advance_ledger_entry(
|
||||
gle, against_voucher_type, against_voucher_no, dr_or_cr_account_currency, cancel
|
||||
)
|
||||
|
||||
ple_map.append(adv)
|
||||
|
||||
ple_map.append(ple)
|
||||
|
||||
return ple_map
|
||||
|
||||
|
||||
def get_advance_ledger_entry(gle, against_voucher_type, against_voucher_no, amount, cancel):
|
||||
event = (
|
||||
"Submit"
|
||||
if (against_voucher_type == gle.voucher_type and against_voucher_no == gle.voucher_no)
|
||||
else "Adjustment"
|
||||
)
|
||||
return frappe._dict(
|
||||
doctype="Advance Payment Ledger Entry",
|
||||
company=gle.company,
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
voucher_detail_no=gle.voucher_detail_no,
|
||||
against_voucher_type=gle.advance_voucher_type,
|
||||
against_voucher_no=gle.advance_voucher_no,
|
||||
amount=amount,
|
||||
currency=gle.account_currency,
|
||||
event=event,
|
||||
delinked=cancel,
|
||||
)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False
|
||||
):
|
||||
@@ -1862,6 +1906,14 @@ def create_payment_ledger_entry(
|
||||
|
||||
|
||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||
if not voucher_type or not voucher_no:
|
||||
return
|
||||
|
||||
if voucher_type in ["Purchase Order", "Sales Order"]:
|
||||
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
|
||||
ref_doc.set_total_advance_paid()
|
||||
return
|
||||
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
|
||||
common_filter = []
|
||||
@@ -1904,7 +1956,27 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
if pl_entry:
|
||||
if not pl_entry:
|
||||
return
|
||||
|
||||
if pl_entry.doctype == "Advance Payment Ledger Entry":
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
|
||||
(
|
||||
qb.update(adv)
|
||||
.set(adv.delinked, 1)
|
||||
.set(adv.event, "Cancel")
|
||||
.set(adv.modified, now())
|
||||
.set(adv.modified_by, frappe.session.user)
|
||||
.where(adv.voucher_type == pl_entry.voucher_type)
|
||||
.where(adv.voucher_no == pl_entry.voucher_no)
|
||||
.where(adv.against_voucher_type == pl_entry.against_voucher_type)
|
||||
.where(adv.against_voucher_no == pl_entry.against_voucher_no)
|
||||
.where(adv.event == pl_entry.event)
|
||||
.run()
|
||||
)
|
||||
|
||||
else:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
qb.update(ple)
|
||||
@@ -2347,3 +2419,39 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
|
||||
"frequency": "Cron",
|
||||
}
|
||||
).save()
|
||||
|
||||
|
||||
def get_link_fields_grouped_by_option(doctype):
|
||||
meta = frappe.get_meta(doctype)
|
||||
link_fields_map = defaultdict(list)
|
||||
|
||||
for df in meta.fields:
|
||||
if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions:
|
||||
link_fields_map[df.options].append(df.fieldname)
|
||||
|
||||
return link_fields_map
|
||||
|
||||
|
||||
def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
match_filters = build_match_conditions(doctype, user, False)
|
||||
link_fields_map = get_link_fields_grouped_by_option(doctype)
|
||||
criterion = []
|
||||
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
|
||||
|
||||
if match_filters:
|
||||
_dt = qb.DocType(doctype)
|
||||
|
||||
for filter in match_filters:
|
||||
for link_option, allowed_values in filter.items():
|
||||
fieldnames = link_fields_map.get(link_option, [])
|
||||
|
||||
for fieldname in fieldnames:
|
||||
field = _dt[fieldname]
|
||||
cond = field.isin(allowed_values)
|
||||
|
||||
if not apply_strict_user_permissions:
|
||||
cond = (Coalesce(field, "") == "") | cond
|
||||
|
||||
criterion.append(cond)
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -249,14 +249,11 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
||||
|
||||
def prepare_depreciation_data(self):
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
def validate_item(self):
|
||||
item = frappe.get_cached_value(
|
||||
@@ -314,6 +311,9 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.calculate_depreciation:
|
||||
return
|
||||
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
@@ -1097,7 +1097,7 @@ def make_journal_entry(asset_name):
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.company = asset.company
|
||||
je.remark = f"Depreciation Entry against asset {asset_name}"
|
||||
je.remark = _("Depreciation Entry against asset {0}").format(asset_name)
|
||||
|
||||
je.append(
|
||||
"accounts",
|
||||
@@ -1215,6 +1215,10 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
|
||||
opening_accumulated_depreciation = flt(
|
||||
(asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity
|
||||
)
|
||||
value_after_depreciation = flt(
|
||||
(asset.value_after_depreciation * remaining_qty) / asset.asset_quantity,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
@@ -1222,6 +1226,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
|
||||
{
|
||||
"opening_accumulated_depreciation": opening_accumulated_depreciation,
|
||||
"gross_purchase_amount": remaining_gross_purchase_amount,
|
||||
"value_after_depreciation": value_after_depreciation,
|
||||
"asset_quantity": remaining_qty,
|
||||
},
|
||||
)
|
||||
@@ -1283,6 +1288,10 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||
new_asset.asset_quantity = split_qty
|
||||
new_asset.split_from = asset.name
|
||||
new_asset.value_after_depreciation = flt(
|
||||
(asset.value_after_depreciation * split_qty) / asset.asset_quantity,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
|
||||
for row in new_asset.get("finance_books"):
|
||||
row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity)
|
||||
@@ -1291,7 +1300,6 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
)
|
||||
|
||||
new_asset.insert()
|
||||
|
||||
add_asset_activity(
|
||||
new_asset.name,
|
||||
_("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)),
|
||||
|
||||
@@ -277,7 +277,9 @@ def _make_journal_entry_for_depreciation(
|
||||
je.posting_date = depr_schedule.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = asset_depr_schedule_doc.finance_book
|
||||
je.remark = f"Depreciation Entry against {asset.name} worth {depr_schedule.depreciation_amount}"
|
||||
je.remark = _("Depreciation Entry against {0} worth {1}").format(
|
||||
asset.name, depr_schedule.depreciation_amount
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
|
||||
@@ -346,6 +346,33 @@ class TestAsset(AssetSetup):
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_asset_status_after_sales_invoice_cancel(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-04-01",
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
|
||||
)
|
||||
asset.load_from_db()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
si.cancel()
|
||||
asset.load_from_db()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_gle_made_by_asset_sale_for_existing_asset(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
@@ -445,6 +472,27 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
def test_asset_splitting_without_depreciation(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=0,
|
||||
asset_quantity=10,
|
||||
available_for_use_date="2020-01-01",
|
||||
purchase_date="2020-01-01",
|
||||
gross_purchase_amount=120000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.asset_quantity, 10)
|
||||
self.assertEqual(asset.gross_purchase_amount, 120000)
|
||||
|
||||
new_asset = split_asset(asset.name, 2)
|
||||
asset.load_from_db()
|
||||
self.assertEqual(asset.asset_quantity, 8)
|
||||
self.assertEqual(asset.gross_purchase_amount, 96000)
|
||||
|
||||
self.assertEqual(new_asset.asset_quantity, 2)
|
||||
self.assertEqual(new_asset.gross_purchase_amount, 24000)
|
||||
|
||||
def test_asset_splitting(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
@@ -1492,7 +1540,6 @@ class TestDepreciationBasics(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 0)
|
||||
|
||||
def test_expected_value_change(self):
|
||||
"""
|
||||
|
||||
@@ -98,20 +98,41 @@ class AssetDepreciationSchedule(Document):
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_asset()
|
||||
self.db_set("status", "Active")
|
||||
|
||||
def before_cancel(self):
|
||||
def validate_asset(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
if not asset.calculate_depreciation:
|
||||
frappe.throw(
|
||||
_("Asset {0} is not set to calculate depreciation.").format(
|
||||
get_link_to_form("Asset", self.asset)
|
||||
)
|
||||
)
|
||||
if asset.docstatus != 1:
|
||||
frappe.throw(
|
||||
_("Asset {0} is not submitted. Please submit the asset before proceeding.").format(
|
||||
get_link_to_form("Asset", self.asset)
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
if not self.flags.should_not_cancel_depreciation_entries:
|
||||
self.cancel_depreciation_entries()
|
||||
|
||||
def cancel_depreciation_entries(self):
|
||||
for d in self.get("depreciation_schedule"):
|
||||
if d.journal_entry:
|
||||
je_status = frappe.db.get_value("Journal Entry", d.journal_entry, "docstatus")
|
||||
if je_status == 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}."
|
||||
).format(self.name, d.journal_entry)
|
||||
)
|
||||
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def update_shift_depr_schedule(self):
|
||||
if not self.shift_based or self.docstatus != 0:
|
||||
return
|
||||
|
||||
@@ -3,13 +3,13 @@ frappe.listview_settings["Asset Maintenance Log"] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function (doc) {
|
||||
if (doc.maintenance_status == "Planned") {
|
||||
return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status];
|
||||
return [__(doc.maintenance_status), "orange", "maintenance_status,=," + doc.maintenance_status];
|
||||
} else if (doc.maintenance_status == "Completed") {
|
||||
return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status];
|
||||
return [__(doc.maintenance_status), "green", "maintenance_status,=," + doc.maintenance_status];
|
||||
} else if (doc.maintenance_status == "Cancelled") {
|
||||
return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status];
|
||||
return [__(doc.maintenance_status), "red", "maintenance_status,=," + doc.maintenance_status];
|
||||
} else if (doc.maintenance_status == "Overdue") {
|
||||
return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status];
|
||||
return [__(doc.maintenance_status), "red", "maintenance_status,=," + doc.maintenance_status];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"fieldname": "completion_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Completion Date",
|
||||
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
@@ -249,7 +250,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-29 22:30:00.589597",
|
||||
"modified": "2025-07-29 15:14:34.044564",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -287,10 +288,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class AssetRepair(AccountsController):
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
|
||||
frappe.throw(
|
||||
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||
)
|
||||
@@ -303,7 +303,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"against_voucher": self.purchase_invoice,
|
||||
"company": self.company,
|
||||
@@ -322,7 +322,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -356,7 +356,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -373,7 +373,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": stock_entry.name,
|
||||
"company": self.company,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
@@ -359,6 +359,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.submit:
|
||||
asset_repair.repair_status = "Completed"
|
||||
asset_repair.completion_date = add_days(args.failure_date, 1)
|
||||
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
|
||||
|
||||
if args.stock_consumption:
|
||||
|
||||
@@ -48,7 +48,6 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.make_depreciation_entry()
|
||||
self.set_value_after_depreciation()
|
||||
self.update_asset(self.new_asset_value)
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
@@ -80,9 +79,6 @@ class AssetValueAdjustment(Document):
|
||||
def set_difference_amount(self):
|
||||
self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
|
||||
|
||||
def set_value_after_depreciation(self):
|
||||
frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value)
|
||||
|
||||
def set_current_asset_value(self):
|
||||
if not self.current_asset_value and self.asset:
|
||||
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
|
||||
@@ -164,12 +160,8 @@ class AssetValueAdjustment(Document):
|
||||
self.db_set("journal_entry", je.name)
|
||||
|
||||
def update_asset(self, asset_value=None):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
if not asset.calculate_depreciation:
|
||||
asset.value_after_depreciation = asset_value
|
||||
asset.save()
|
||||
return
|
||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||
asset = self.update_asset_value_after_depreciation(difference_amount)
|
||||
|
||||
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
|
||||
|
||||
@@ -188,19 +180,6 @@ class AssetValueAdjustment(Document):
|
||||
get_link_to_form(self.get("doctype"), self.get("name")),
|
||||
)
|
||||
|
||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
asset.db_update()
|
||||
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
asset,
|
||||
notes,
|
||||
@@ -212,6 +191,23 @@ class AssetValueAdjustment(Document):
|
||||
asset.save()
|
||||
asset.set_status()
|
||||
|
||||
def update_asset_value_after_depreciation(self, difference_amount):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation = row.value_after_depreciation + flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
asset.value_after_depreciation += flt(difference_amount)
|
||||
asset.db_update()
|
||||
return asset
|
||||
|
||||
def get_adjusted_salvage_value_amount(self, row, difference_amount):
|
||||
if row.expected_value_after_useful_life:
|
||||
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
|
||||
|
||||
@@ -282,13 +282,13 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=54000,
|
||||
current_asset_value=120000.0,
|
||||
new_asset_value=50000.0,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -4000)
|
||||
self.assertEqual(difference_amount, -70000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ erpnext.buying.setup_buying_controller();
|
||||
|
||||
frappe.ui.form.on("Purchase Order", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function () {
|
||||
return {
|
||||
@@ -140,6 +139,10 @@ frappe.ui.form.on("Purchase Order", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
var ignore_list = ["Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
frm.ignore_doctypes_on_cancel_all = Object.hasOwn(frm, "ignore_doctypes_on_cancel_all")
|
||||
? frm.ignore_doctypes_on_cancel_all.concat(ignore_list)
|
||||
: ignore_list;
|
||||
set_schedule_date(frm);
|
||||
if (!frm.doc.transaction_date) {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
|
||||
@@ -505,6 +505,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
@@ -719,6 +720,7 @@ def close_or_unclose_purchase_orders(names, status):
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
target.run_method("set_use_serial_batch_fields")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_columns(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Requestor"),
|
||||
"options": "Employee",
|
||||
"options": "User",
|
||||
"fieldname": "requestor",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
|
||||
@@ -397,7 +397,6 @@ class AccountsController(TransactionBase):
|
||||
def on_trash(self):
|
||||
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
|
||||
|
||||
self._remove_advance_payment_ledger_entries()
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
@@ -426,6 +425,8 @@ class AccountsController(TransactionBase):
|
||||
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
|
||||
).run()
|
||||
|
||||
self._remove_advance_payment_ledger_entries()
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
@@ -1946,6 +1947,15 @@ class AccountsController(TransactionBase):
|
||||
and self.get("discount_amount")
|
||||
and self.get("additional_discount_account")
|
||||
):
|
||||
# cases where distributed_discount_amount is not patched
|
||||
if not hasattr(self, "__has_distributed_discount_set"):
|
||||
self.__has_distributed_discount_set = any(
|
||||
i.distributed_discount_amount for i in self.get("items")
|
||||
)
|
||||
|
||||
if not self.__has_distributed_discount_set:
|
||||
return item.amount, item.base_amount
|
||||
|
||||
amount += item.distributed_discount_amount
|
||||
base_amount += flt(
|
||||
item.distributed_discount_amount * self.get("conversion_rate"),
|
||||
@@ -2224,54 +2234,30 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def calculate_total_advance_from_ledger(self):
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
advance = (
|
||||
frappe.qb.from_(adv)
|
||||
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount"))
|
||||
.where(
|
||||
(adv.against_voucher_type == self.doctype)
|
||||
& (adv.against_voucher_no == self.name)
|
||||
& (adv.company == self.company)
|
||||
)
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
|
||||
.where(adv.company == self.company)
|
||||
.where(adv.delinked == 0)
|
||||
.where(adv.against_voucher_type == self.doctype)
|
||||
.where(adv.against_voucher_no == self.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return advance
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
advance = self.calculate_total_advance_from_ledger()
|
||||
advance_paid, order_total = None, None
|
||||
advance_paid = 0
|
||||
|
||||
if advance:
|
||||
advance = advance[0]
|
||||
|
||||
advance_paid = flt(advance.amount, self.precision("advance_paid"))
|
||||
formatted_advance_paid = fmt_money(
|
||||
advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency
|
||||
)
|
||||
|
||||
if advance.account_currency:
|
||||
frappe.db.set_value(
|
||||
self.doctype, self.name, "party_account_currency", advance.account_currency
|
||||
)
|
||||
|
||||
if advance.account_currency == self.currency:
|
||||
order_total = self.get("rounded_total") or self.grand_total
|
||||
precision = "rounded_total" if self.get("rounded_total") else "grand_total"
|
||||
else:
|
||||
order_total = self.get("base_rounded_total") or self.base_grand_total
|
||||
precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total"
|
||||
|
||||
formatted_order_total = fmt_money(
|
||||
order_total, precision=self.precision(precision), currency=advance.account_currency
|
||||
)
|
||||
|
||||
if self.currency == self.company_currency and advance_paid > order_total:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})"
|
||||
).format(formatted_advance_paid, self.name, formatted_order_total)
|
||||
)
|
||||
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
|
||||
@property
|
||||
def company_abbr(self):
|
||||
@@ -2915,64 +2901,6 @@ class AccountsController(TransactionBase):
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return _get_advance_payment_doctypes()
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.accounts if x.reference_type in advance_payment_doctypes
|
||||
]
|
||||
|
||||
for x in advance_doctype_references:
|
||||
# Looking for payments
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if x.account_type == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
amount = x.get(dr_or_cr)
|
||||
if amount > 0:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_type
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = amount if self.docstatus == 1 else -1 * amount
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.currency = x.account_currency
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_for_payment(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.references if x.reference_doctype in advance_payment_doctypes
|
||||
]
|
||||
currency = (
|
||||
self.paid_from_account_currency
|
||||
if self.payment_type == "Receive"
|
||||
else self.paid_to_account_currency
|
||||
)
|
||||
for x in advance_doctype_references:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_doctype
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount
|
||||
doc.currency = currency
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_entries(self):
|
||||
if self.docstatus != 0:
|
||||
if self.doctype == "Journal Entry":
|
||||
self.make_advance_payment_ledger_for_journal()
|
||||
elif self.doctype == "Payment Entry":
|
||||
self.make_advance_payment_ledger_for_payment()
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
|
||||
for x in gl_entries:
|
||||
x["transaction_currency"] = self.currency
|
||||
|
||||
@@ -310,9 +310,12 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
|
||||
total_rejected_qty = 0
|
||||
for d in self.get("items"):
|
||||
if d.item_code and d.item_code in stock_and_asset_items:
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
total_rejected_qty += flt(d.get("rejected_qty", 0))
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
last_item_idx = d.idx
|
||||
|
||||
@@ -324,12 +327,19 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
valuation_amount_adjustment = total_valuation_amount
|
||||
for i, item in enumerate(self.get("items")):
|
||||
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
|
||||
item_proportion = (
|
||||
flt(item.base_net_amount) / stock_and_asset_items_amount
|
||||
if stock_and_asset_items_amount
|
||||
else flt(item.qty) / stock_and_asset_items_qty
|
||||
)
|
||||
if (
|
||||
item.item_code
|
||||
and (item.qty or item.get("rejected_qty"))
|
||||
and item.item_code in stock_and_asset_items
|
||||
):
|
||||
if stock_and_asset_items_qty:
|
||||
item_proportion = (
|
||||
flt(item.base_net_amount) / stock_and_asset_items_amount
|
||||
if stock_and_asset_items_amount
|
||||
else flt(item.qty) / stock_and_asset_items_qty
|
||||
)
|
||||
elif total_rejected_qty:
|
||||
item_proportion = flt(item.get("rejected_qty")) / flt(total_rejected_qty)
|
||||
|
||||
if i == (last_item_idx - 1):
|
||||
item.item_tax_amount = flt(
|
||||
@@ -351,7 +361,19 @@ class BuyingController(SubcontractingController):
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
and item.get("rejected_qty")
|
||||
and frappe.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
):
|
||||
net_rate = item.rejected_qty * item.net_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
if not qty_in_stock_uom and item.get("rejected_qty"):
|
||||
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
|
||||
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
@@ -447,13 +469,12 @@ class BuyingController(SubcontractingController):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
d.sales_incoming_rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
* (d.conversion_factor or 1)
|
||||
)
|
||||
|
||||
def validate_for_subcontracting(self):
|
||||
|
||||
@@ -42,14 +42,14 @@ def employee_query(
|
||||
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
|
||||
)
|
||||
|
||||
search_conditions = " or ".join([f"{field} like %(txt)s" for field in fields])
|
||||
mcond = "" if ignore_permissions else get_match_cond(doctype)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
and ({key} like %(txt)s or {search_conditions})
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
@@ -62,6 +62,7 @@ def employee_query(
|
||||
"key": searchfield,
|
||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||
"mcond": mcond,
|
||||
"search_conditions": search_conditions,
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
@@ -36,6 +36,18 @@ def validate_return_against(doc):
|
||||
|
||||
party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
|
||||
|
||||
if ref_doc.get(party_type) != doc.get(party_type):
|
||||
frappe.throw(
|
||||
_("The {0} {1} does not match with the {0} {2} in the {3} {4}").format(
|
||||
doc.meta.get_label(party_type),
|
||||
bold(doc.get(party_type)),
|
||||
bold(ref_doc.get(party_type)),
|
||||
ref_doc.doctype,
|
||||
ref_doc.name,
|
||||
),
|
||||
title=_("Party Mismatch"),
|
||||
)
|
||||
|
||||
if (
|
||||
ref_doc.company == doc.company
|
||||
and ref_doc.get(party_type) == doc.get(party_type)
|
||||
|
||||
@@ -57,6 +57,38 @@ class SellingController(StockController):
|
||||
if self.get(table_field):
|
||||
self.set_serial_and_batch_bundle(table_field)
|
||||
|
||||
def validate_standalone_serial_nos_customer(self):
|
||||
if not self.is_return or self.return_against:
|
||||
return
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||
bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle]
|
||||
if not bundle_ids:
|
||||
return
|
||||
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundle_ids), "serial_no": ("is", "set")},
|
||||
pluck="serial_no",
|
||||
)
|
||||
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
if serial_nos := frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", serial_nos), "customer": ("is", "set")},
|
||||
fields=["name", "customer"],
|
||||
):
|
||||
for sn in serial_nos:
|
||||
if sn.customer and sn.customer != self.customer:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}"
|
||||
).format(frappe.bold(sn.name), frappe.bold(sn.customer)),
|
||||
title=_("Serial No Already Assigned"),
|
||||
)
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ status_map = {
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
[
|
||||
"Completed",
|
||||
"eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
|
||||
"eval:(self.per_billed >= 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
|
||||
],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
|
||||
@@ -638,7 +638,9 @@ class StockController(AccountsController):
|
||||
).format(wh, self.company)
|
||||
)
|
||||
|
||||
return process_gl_map(gl_list, precision=precision)
|
||||
return process_gl_map(
|
||||
gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation
|
||||
)
|
||||
|
||||
def get_debit_field_precision(self):
|
||||
if not frappe.flags.debit_field_precision:
|
||||
|
||||
@@ -464,7 +464,7 @@ class SubcontractingController(StockController):
|
||||
i += 1
|
||||
|
||||
def __remove_serial_and_batch_bundle(self, item):
|
||||
if item.serial_and_batch_bundle:
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
@@ -953,7 +953,7 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
|
||||
sco_doc.update_reserved_qty_for_subcontracting()
|
||||
sco_doc.update_reserved_qty_for_subcontracting(sco_item_rows)
|
||||
|
||||
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
|
||||
if hasattr(self, "supplied_items"):
|
||||
@@ -1046,7 +1046,7 @@ class SubcontractingController(StockController):
|
||||
|
||||
return supplied_items_cost
|
||||
|
||||
def set_subcontracting_order_status(self):
|
||||
def set_subcontracting_order_status(self, update_bin=True):
|
||||
if self.doctype == "Subcontracting Order":
|
||||
self.update_status()
|
||||
elif self.doctype == "Subcontracting Receipt":
|
||||
@@ -1055,7 +1055,7 @@ class SubcontractingController(StockController):
|
||||
if self.subcontract_orders:
|
||||
for sco in set(self.subcontract_orders):
|
||||
sco_doc = frappe.get_doc("Subcontracting Order", sco)
|
||||
sco_doc.update_status()
|
||||
sco_doc.update_status(update_bin=update_bin)
|
||||
|
||||
def calculate_additional_costs(self):
|
||||
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
|
||||
|
||||
@@ -78,11 +78,11 @@ class EmailCampaign(Document):
|
||||
end_date = getdate(self.end_date)
|
||||
today_date = getdate(today())
|
||||
if start_date > today_date:
|
||||
self.status = "Scheduled"
|
||||
self.db_set("status", "Scheduled", update_modified=False)
|
||||
elif end_date >= today_date:
|
||||
self.status = "In Progress"
|
||||
self.db_set("status", "In Progress", update_modified=False)
|
||||
elif end_date < today_date:
|
||||
self.status = "Completed"
|
||||
self.db_set("status", "Completed", update_modified=False)
|
||||
|
||||
|
||||
# called through hooks to send campaign mails to leads
|
||||
|
||||
@@ -153,7 +153,12 @@ class OpportunitySummaryBySalesStage:
|
||||
}[self.filters.get("based_on")]
|
||||
|
||||
if self.filters.get("based_on") == "Opportunity Owner":
|
||||
if d.get(based_on) == "[]" or d.get(based_on) is None or d.get(based_on) == "Not Assigned":
|
||||
if (
|
||||
d.get(based_on) == "[]"
|
||||
or d.get(based_on) is None
|
||||
or d.get(based_on) == "Not Assigned"
|
||||
or d.get(based_on) == ""
|
||||
):
|
||||
assignments = ["Not Assigned"]
|
||||
else:
|
||||
assignments = json.loads(d.get(based_on))
|
||||
|
||||
@@ -60,7 +60,7 @@ def import_genericode():
|
||||
"doctype": "File",
|
||||
"attached_to_doctype": "Code List",
|
||||
"attached_to_name": code_list.name,
|
||||
"folder": "Home/Attachments",
|
||||
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
|
||||
@@ -1274,7 +1274,7 @@ def get_children(parent=None, is_root=False, **filters):
|
||||
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["item_code", "bom_no as value", "stock_qty"],
|
||||
fields=["item_code", "bom_no as value", "stock_qty", "qty"],
|
||||
filters=[["parent", "=", frappe.form_dict.parent]],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user