mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-27 21:08:33 +00:00
Compare commits
285 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20a26c3dd4 | ||
|
|
d975eccaab | ||
|
|
cdef0bd503 | ||
|
|
b7fd24074d | ||
|
|
d53a9f6530 | ||
|
|
4908d03a88 | ||
|
|
214861d68f | ||
|
|
e7b96209cd | ||
|
|
753a1b511d | ||
|
|
6715dd6167 | ||
|
|
a061798a6f | ||
|
|
fa79a7cdab | ||
|
|
6884aac2a0 | ||
|
|
e61a12b4cb | ||
|
|
4542a12787 | ||
|
|
f0408c89ce | ||
|
|
7c080bd60c | ||
|
|
c87a3c7793 | ||
|
|
7813efad35 | ||
|
|
29cd474f44 | ||
|
|
008400d287 | ||
|
|
4b9d9c0df4 | ||
|
|
b28deaae2c | ||
|
|
4e6b8ead8f | ||
|
|
c2a137f0fb | ||
|
|
f6fcb7ee26 | ||
|
|
973d06d96f | ||
|
|
37bf57cf75 | ||
|
|
8d4c38f6b9 | ||
|
|
01b2a1ffeb | ||
|
|
b858d1ceeb | ||
|
|
14b908f8ab | ||
|
|
25ec1fce61 | ||
|
|
70965efd94 | ||
|
|
9f43e9e668 | ||
|
|
47ef075759 | ||
|
|
4544727654 | ||
|
|
6c89f351f4 | ||
|
|
35c06fabc5 | ||
|
|
cbe15e159a | ||
|
|
3411814ee3 | ||
|
|
12d3cda2d9 | ||
|
|
efc6a8a044 | ||
|
|
0d913a6401 | ||
|
|
2e3a860a46 | ||
|
|
c07723d49d | ||
|
|
7551ba3851 | ||
|
|
67f149ee06 | ||
|
|
d27e588672 | ||
|
|
2e333e6802 | ||
|
|
59773acdd2 | ||
|
|
454d789232 | ||
|
|
c349c89ae3 | ||
|
|
36dc7bd55e | ||
|
|
5a8bf7bf7d | ||
|
|
1042d4cfeb | ||
|
|
9f23b7e46f | ||
|
|
b7fe163de5 | ||
|
|
8c9f4c8700 | ||
|
|
419943a9d9 | ||
|
|
eee9062814 | ||
|
|
6db63c971e | ||
|
|
b88c7d63de | ||
|
|
9345334196 | ||
|
|
ae285f3417 | ||
|
|
b813964ee3 | ||
|
|
3f311fcc1e | ||
|
|
8d6d74c237 | ||
|
|
0df52d2c4f | ||
|
|
497049b3fd | ||
|
|
bec7fb5493 | ||
|
|
7b6f25cb9e | ||
|
|
865288976c | ||
|
|
00347cdc8e | ||
|
|
161b0da29a | ||
|
|
b7556f9b30 | ||
|
|
2b570ed68c | ||
|
|
d82a123cf8 | ||
|
|
e8ea6022f0 | ||
|
|
2908d966b6 | ||
|
|
2dc4b02fc8 | ||
|
|
f9f65a53fb | ||
|
|
ecde1d58b5 | ||
|
|
7fa97fa7b5 | ||
|
|
72db68dd39 | ||
|
|
d0224cecac | ||
|
|
49296cd5cb | ||
|
|
55e4951445 | ||
|
|
8e370876cc | ||
|
|
c71b31e3b8 | ||
|
|
44b11750d3 | ||
|
|
d337533907 | ||
|
|
fd6a23cf41 | ||
|
|
cec25740ea | ||
|
|
9170ffc371 | ||
|
|
6c805fff15 | ||
|
|
53d083a121 | ||
|
|
d8c3935e64 | ||
|
|
47d37aa628 | ||
|
|
6260241c55 | ||
|
|
6871331ff3 | ||
|
|
bb2aa904b2 | ||
|
|
e490bd7785 | ||
|
|
ca6a78882c | ||
|
|
321ac4c5f0 | ||
|
|
3929cc30ca | ||
|
|
25f67923ea | ||
|
|
5dc7faba0c | ||
|
|
2aa9171027 | ||
|
|
730a3bb7ce | ||
|
|
e9a2b002d8 | ||
|
|
faf025454f | ||
|
|
50f3b01eb7 | ||
|
|
f777d24679 | ||
|
|
d72fbe9125 | ||
|
|
3ad6d0890a | ||
|
|
776c884403 | ||
|
|
a4b5fc8100 | ||
|
|
b0547b5619 | ||
|
|
9cf8955d24 | ||
|
|
f7c820ffad | ||
|
|
1773e31499 | ||
|
|
466d0849e8 | ||
|
|
28b6afcb10 | ||
|
|
bdde30a456 | ||
|
|
aea169d4d4 | ||
|
|
b0aa4efd3d | ||
|
|
7dce68bdcc | ||
|
|
436ba6f244 | ||
|
|
79ded4bf19 | ||
|
|
5af23e760c | ||
|
|
7601861f21 | ||
|
|
1d1af26f7a | ||
|
|
47093bebd3 | ||
|
|
205d971253 | ||
|
|
49a787b1a8 | ||
|
|
a88d322e4f | ||
|
|
dfdaad0111 | ||
|
|
d80e8c6b15 | ||
|
|
d7d5317b0e | ||
|
|
445e9c9041 | ||
|
|
e96ef7338e | ||
|
|
ae960e735a | ||
|
|
fd19ac55e3 | ||
|
|
8a3a0688ba | ||
|
|
8d5efc901d | ||
|
|
5dcd806176 | ||
|
|
0448d56a4f | ||
|
|
f74132b0be | ||
|
|
6353deacf0 | ||
|
|
756f08b9ba | ||
|
|
2293696ab4 | ||
|
|
06c6da5c0d | ||
|
|
5261aba81f | ||
|
|
f170cb9214 | ||
|
|
8055773425 | ||
|
|
82fa58d4e3 | ||
|
|
57c6d616e6 | ||
|
|
639512a7c2 | ||
|
|
ac13e7362c | ||
|
|
1e7dfd3071 | ||
|
|
e7183e3ea9 | ||
|
|
c4e57040b7 | ||
|
|
c1a7d614a2 | ||
|
|
7392f9c662 | ||
|
|
aa6ef76043 | ||
|
|
f6f5ff0be9 | ||
|
|
00def82843 | ||
|
|
e08f1145c9 | ||
|
|
ddf82cf433 | ||
|
|
59a5e132fe | ||
|
|
7fc7105c48 | ||
|
|
3daf6f822a | ||
|
|
d7278477d6 | ||
|
|
d0571530eb | ||
|
|
392ee2e0fd | ||
|
|
562a55122b | ||
|
|
b226adeb2f | ||
|
|
12f027b3f4 | ||
|
|
9f8aa7c59d | ||
|
|
40f904391e | ||
|
|
bfb85527dc | ||
|
|
b868e2bd5b | ||
|
|
b7e74b479f | ||
|
|
7c92ecdc9d | ||
|
|
80bc235d6e | ||
|
|
883eaee014 | ||
|
|
ae319760a0 | ||
|
|
4e2d0aa892 | ||
|
|
5afb8b59c9 | ||
|
|
8136657dfc | ||
|
|
9b5185adc9 | ||
|
|
296433a1dd | ||
|
|
5033f189f9 | ||
|
|
d85185db2e | ||
|
|
76b6e0407d | ||
|
|
2fdc575441 | ||
|
|
7b9afa43ff | ||
|
|
49735bc120 | ||
|
|
a70696ea77 | ||
|
|
2849e0daed | ||
|
|
da27c0fb2d | ||
|
|
430980a836 | ||
|
|
51e33e1556 | ||
|
|
8d4a19cecf | ||
|
|
9a34518e54 | ||
|
|
39841f9edd | ||
|
|
1ccd5e4ff5 | ||
|
|
05d514ca8e | ||
|
|
c7c751ecd0 | ||
|
|
fa1f623801 | ||
|
|
8dbe753a25 | ||
|
|
a060000b70 | ||
|
|
b615cfc540 | ||
|
|
32b6eed691 | ||
|
|
ad03008a1a | ||
|
|
2ec1a2f862 | ||
|
|
f5f48f7480 | ||
|
|
8dbf2ced79 | ||
|
|
b951dbfced | ||
|
|
863a200989 | ||
|
|
8dafe103bd | ||
|
|
eda9800c93 | ||
|
|
6d983b5f45 | ||
|
|
65614497ed | ||
|
|
785bec98d3 | ||
|
|
6b08aa2198 | ||
|
|
dd76695d3a | ||
|
|
236a064cd3 | ||
|
|
dd685e50df | ||
|
|
85853295c4 | ||
|
|
4eb37cce26 | ||
|
|
d0698b32bb | ||
|
|
bc01007c16 | ||
|
|
fb2ca3e172 | ||
|
|
7d66cb2195 | ||
|
|
58cb4303ee | ||
|
|
56c70d300b | ||
|
|
1f16c47a2c | ||
|
|
7b14e9d5f6 | ||
|
|
966adfc081 | ||
|
|
d1a8202624 | ||
|
|
7a2df24129 | ||
|
|
ecb116aa99 | ||
|
|
8446e87cc2 | ||
|
|
fb19e7f15e | ||
|
|
c840f30a20 | ||
|
|
0813f02ca5 | ||
|
|
a7e2867526 | ||
|
|
077f3928cf | ||
|
|
c5528844fd | ||
|
|
55885e84ab | ||
|
|
2faf2ce468 | ||
|
|
7ab18fc434 | ||
|
|
a104a4bca6 | ||
|
|
6979c3e490 | ||
|
|
57ce73d540 | ||
|
|
424ebd154a | ||
|
|
7c7dc5328e | ||
|
|
cb655226de | ||
|
|
010ccf80f5 | ||
|
|
13ea910fa6 | ||
|
|
9c0d80cef4 | ||
|
|
620f4f2c10 | ||
|
|
89695022c7 | ||
|
|
50627733b4 | ||
|
|
7942ad488e | ||
|
|
c3d7348c73 | ||
|
|
ad178db28f | ||
|
|
984703c3c9 | ||
|
|
6b34568128 | ||
|
|
cf0d936195 | ||
|
|
0eab723243 | ||
|
|
935286ff94 | ||
|
|
db29180eec | ||
|
|
b5bed65d9e | ||
|
|
3f7f5bd8f0 | ||
|
|
043dc1b14b | ||
|
|
c5a6b17c15 | ||
|
|
16c3f3bcbd | ||
|
|
b0a184e937 | ||
|
|
21b430a575 | ||
|
|
2c1fdecc91 | ||
|
|
4d2169e48e | ||
|
|
b75aae99ca |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.47.2"
|
||||
__version__ = "14.54.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa"
|
||||
"show_balance_in_coa",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -429,6 +434,34 @@
|
||||
"fieldname": "show_balance_in_coa",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Balances in Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "reports_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Reports"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Truncates 'Remarks' column to set character length",
|
||||
"fieldname": "general_ledger_remarks_length",
|
||||
"fieldtype": "Int",
|
||||
"label": "General Ledger"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Truncates 'Remarks' column to set character length",
|
||||
"fieldname": "receivable_payable_remarks_length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Accounts Receivable/Payable"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lvjk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Remarks Column Length"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -436,7 +469,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-27 15:05:34.000264",
|
||||
"modified": "2023-11-20 09:37:47.650347",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -112,7 +112,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
|
||||
field = party.lower() + "_name"
|
||||
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
if not self.get(field):
|
||||
@@ -131,7 +132,11 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
|
||||
skip = False
|
||||
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
|
||||
result = process.extract(
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
@@ -149,14 +154,14 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
Returns: Result, Skip (whether or not to discontinue matching)
|
||||
"""
|
||||
PARTY, SCORE, CUTOFF = 0, 1, 80
|
||||
SCORE, PARTY_ID, CUTOFF = 1, 2, 80
|
||||
|
||||
if not result or not len(result):
|
||||
return None, False
|
||||
|
||||
first_result = result[0]
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
|
||||
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
@@ -165,7 +170,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
return None, True
|
||||
|
||||
return first_result[PARTY], True
|
||||
return first_result[PARTY_ID], True
|
||||
else:
|
||||
return None, False
|
||||
|
||||
|
||||
@@ -53,10 +53,18 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
||||
of Accounts. Please enter the account names and add more rows as per your requirement.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label : "Company",
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
reqd: 1,
|
||||
hidden: 1,
|
||||
default: frm.doc.company,
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let data = d.get_values();
|
||||
|
||||
if (!data.template_type) {
|
||||
frappe.throw(__('Please select <b>Template Type</b> to download template'));
|
||||
@@ -66,7 +74,8 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
||||
'/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template',
|
||||
{
|
||||
file_type: data.file_type,
|
||||
template_type: data.template_type
|
||||
template_type: data.template_type,
|
||||
company: data.company
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form.linked_with import get_linked_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils.csvutils import UnicodeWriter
|
||||
@@ -112,7 +113,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
if as_dict:
|
||||
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
|
||||
else:
|
||||
if not row[1]:
|
||||
if not row[1] and len(row) > 1:
|
||||
row[1] = row[0]
|
||||
row[3] = row[2]
|
||||
data.append(row)
|
||||
@@ -294,10 +295,8 @@ def build_response_as_excel(writer):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_template(file_type, template_type):
|
||||
data = frappe._dict(frappe.local.form_dict)
|
||||
|
||||
writer = get_template(template_type)
|
||||
def download_template(file_type, template_type, company):
|
||||
writer = get_template(template_type, company)
|
||||
|
||||
if file_type == "CSV":
|
||||
# download csv file
|
||||
@@ -308,8 +307,7 @@ def download_template(file_type, template_type):
|
||||
build_response_as_excel(writer)
|
||||
|
||||
|
||||
def get_template(template_type):
|
||||
|
||||
def get_template(template_type, company):
|
||||
fields = [
|
||||
"Account Name",
|
||||
"Parent Account",
|
||||
@@ -335,34 +333,17 @@ def get_template(template_type):
|
||||
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
)
|
||||
else:
|
||||
writer = get_sample_template(writer)
|
||||
writer = get_sample_template(writer, company)
|
||||
|
||||
return writer
|
||||
|
||||
|
||||
def get_sample_template(writer):
|
||||
template = [
|
||||
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
|
||||
["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"],
|
||||
["Equity", "", "", "", 1, "", "Equity"],
|
||||
["Expenses", "", "", "", 1, "", "Expense"],
|
||||
["Income", "", "", "", 1, "", "Income"],
|
||||
["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"],
|
||||
["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"],
|
||||
["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"],
|
||||
["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"],
|
||||
["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"],
|
||||
["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"],
|
||||
["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"],
|
||||
["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"],
|
||||
["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"],
|
||||
["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"],
|
||||
["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"],
|
||||
["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"],
|
||||
]
|
||||
|
||||
for row in template:
|
||||
writer.writerow(row)
|
||||
def get_sample_template(writer, company):
|
||||
currency = frappe.db.get_value("Company", company, "default_currency")
|
||||
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f:
|
||||
for row in f:
|
||||
row = row.strip().split(",") + [currency]
|
||||
writer.writerow(row)
|
||||
|
||||
return writer
|
||||
|
||||
@@ -453,14 +434,11 @@ def get_mandatory_account_types():
|
||||
|
||||
|
||||
def unset_existing_data(company):
|
||||
linked = frappe.db.sql(
|
||||
'''select fieldname from tabDocField
|
||||
where fieldtype="Link" and options="Account" and parent="Company"''',
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
# remove accounts data from company
|
||||
update_values = {d.fieldname: "" for d in linked}
|
||||
|
||||
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
|
||||
linked = [{"fieldname": name} for name in fieldnames]
|
||||
update_values = {d.get("fieldname"): "" for d in linked}
|
||||
frappe.db.set_value("Company", company, update_values, update_values)
|
||||
|
||||
# remove accounts data from various doctypes
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Application Of Funds(Assets),,,,1,,Asset
|
||||
Sources Of Funds(Liabilities),,,,1,,Liability
|
||||
Equity,,,,1,,Equity
|
||||
Expenses,,,,1,Expense Account,Expense
|
||||
Income,,,,1,Income Account,Income
|
||||
Bank Accounts,Application Of Funds(Assets),,,1,Bank,Asset
|
||||
Cash In Hand,Application Of Funds(Assets),,,1,Cash,Asset
|
||||
Stock Assets,Application Of Funds(Assets),,,1,Stock,Asset
|
||||
Cost Of Goods Sold,Expenses,,,0,Cost of Goods Sold,Expense
|
||||
Asset Depreciation,Expenses,,,0,Depreciation,Expense
|
||||
Fixed Assets,Application Of Funds(Assets),,,0,Fixed Asset,Asset
|
||||
Accounts Payable,Sources Of Funds(Liabilities),,,0,Payable,Liability
|
||||
Accounts Receivable,Application Of Funds(Assets),,,1,Receivable,Asset
|
||||
Stock Expenses,Expenses,,,0,Stock Adjustment,Expense
|
||||
Sample Bank,Bank Accounts,,,0,Bank,Asset
|
||||
Cash,Cash In Hand,,,0,Cash,Asset
|
||||
Stores,Stock Assets,,,0,Stock,Asset
|
||||
|
@@ -192,7 +192,7 @@ class ExchangeRateRevaluation(Document):
|
||||
# round off balance based on currency precision
|
||||
# and consider debit-credit difference allowance
|
||||
currency_precision = get_currency_precision()
|
||||
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
|
||||
rounding_loss_allowance = float(rounding_loss_allowance)
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@@ -51,7 +51,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}, __('Make'));
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
},
|
||||
before_save: function(frm) {
|
||||
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
|
||||
|
||||
@@ -548,8 +548,16 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 14:32:22.366895",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Bank Transaction Payments",
|
||||
"link_fieldname": "payment_entry",
|
||||
"parent_doctype": "Bank Transaction",
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -98,6 +98,8 @@ class JournalEntry(AccountsController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
@@ -496,7 +498,7 @@ class JournalEntry(AccountsController):
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
else:
|
||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||
dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
|
||||
valid = False
|
||||
for jvd in against_entries:
|
||||
if flt(jvd[dr_or_cr]) > 0:
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
@@ -211,7 +212,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type"
|
||||
"options": "reference_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
@@ -278,13 +280,14 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
"no_copy": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-16 14:11:13.507807",
|
||||
"modified": "2023-11-23 11:44:25.841187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@@ -158,7 +158,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}, __('Actions'));
|
||||
|
||||
}
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -829,7 +829,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
else
|
||||
total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
||||
})
|
||||
|
||||
var allocated_negative_outstanding = 0;
|
||||
if (
|
||||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
|
||||
@@ -844,6 +843,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
|
||||
if(paid_amount > total_negative_outstanding) {
|
||||
if(total_negative_outstanding == 0) {
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -739,8 +739,16 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-19 11:38:04.387219",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Bank Transaction Payments",
|
||||
"link_fieldname": "payment_entry",
|
||||
"parent_doctype": "Bank Transaction",
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:07:20.887885",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -786,4 +794,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||
@@ -107,7 +109,7 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
@@ -913,8 +915,11 @@ class PaymentEntry(AccountsController):
|
||||
):
|
||||
return
|
||||
|
||||
total_negative_outstanding = sum(
|
||||
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
|
||||
total_negative_outstanding = flt(
|
||||
sum(
|
||||
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
|
||||
),
|
||||
self.references[0].precision("outstanding_amount") if self.references else None,
|
||||
)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
|
||||
@@ -1549,13 +1554,43 @@ def get_outstanding_reference_documents(args):
|
||||
return data
|
||||
|
||||
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
invoice_ref_based_on_payment_terms = {}
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
|
||||
"""Split a list of invoices based on their payment terms."""
|
||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
for entry in outstanding_invoices:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if payment_term_template := frappe.db.get_value(
|
||||
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
||||
):
|
||||
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
|
||||
if not split_rows:
|
||||
continue
|
||||
|
||||
if len(split_rows) > 1:
|
||||
frappe.msgprint(
|
||||
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
||||
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
outstanding_invoices_after_split += split_rows
|
||||
continue
|
||||
|
||||
# If not an invoice or no payment terms template, add as it is
|
||||
outstanding_invoices_after_split.append(entry)
|
||||
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
exc_rates = frappe._dict()
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
for x in frappe.db.get_all(
|
||||
@@ -1570,71 +1605,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
company_currency=company_currency,
|
||||
)
|
||||
|
||||
for idx, d in enumerate(outstanding_invoices):
|
||||
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_term_template = frappe.db.get_value(
|
||||
d.voucher_type, d.voucher_no, "payment_terms_template"
|
||||
return exc_rates
|
||||
|
||||
|
||||
def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
|
||||
"""Split invoice based on its payment schedule table."""
|
||||
split_rows = []
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not allocate_payment_based_on_payment_terms:
|
||||
return [invoice]
|
||||
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
|
||||
)
|
||||
for payment_term in payment_schedule:
|
||||
if not payment_term.outstanding > 0.1:
|
||||
continue
|
||||
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
split_rows.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": invoice.due_date,
|
||||
"currency": invoice.currency,
|
||||
"voucher_no": invoice.voucher_no,
|
||||
"voucher_type": invoice.voucher_type,
|
||||
"posting_date": invoice.posting_date,
|
||||
"invoice_amount": flt(invoice.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else invoice.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
)
|
||||
if payment_term_template:
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
if allocate_payment_based_on_payment_terms:
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
|
||||
)
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
if payment_term.outstanding > 0.1:
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
invoice_ref_based_on_payment_terms.setdefault(idx, [])
|
||||
invoice_ref_based_on_payment_terms[idx].append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = ref[0]["voucher_no"]
|
||||
voucher_type = ref[0]["voucher_type"]
|
||||
|
||||
frappe.msgprint(
|
||||
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
return split_rows
|
||||
|
||||
|
||||
def get_orders_to_be_billed(
|
||||
@@ -1838,18 +1856,24 @@ def get_company_defaults(company):
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
res = frappe.db.sql(
|
||||
"SELECT "
|
||||
'CASE WHEN party_type IN ("Customer") '
|
||||
"THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
|
||||
"ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
|
||||
"END as outstanding_amount "
|
||||
"FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) "
|
||||
"AND party_type IS NOT NULL "
|
||||
'AND party_type != ""',
|
||||
(name, name),
|
||||
as_dict=1,
|
||||
)
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
res = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
Case()
|
||||
.when(
|
||||
gl.party_type == "Customer",
|
||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
||||
)
|
||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
||||
.as_("outstanding_amount")
|
||||
)
|
||||
.where(
|
||||
(Coalesce(gl.party_type, "") != "")
|
||||
& (gl.is_cancelled == 0)
|
||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_outstanding_reference_documents,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
@@ -21,6 +23,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
create_sales_invoice,
|
||||
create_sales_invoice_against_cost_center,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
|
||||
@@ -1219,6 +1222,115 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, so.rounded_total)
|
||||
|
||||
def test_outstanding_invoices_api(self):
|
||||
"""
|
||||
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
|
||||
"""
|
||||
customer = create_customer("Max Mustermann", "INR")
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
|
||||
args = {
|
||||
"posting_date": nowdate(),
|
||||
"company": "_Test Company",
|
||||
"party_type": "Customer",
|
||||
"payment_type": "Pay",
|
||||
"party": customer,
|
||||
"party_account": "Debtors - _TC",
|
||||
}
|
||||
args.update(
|
||||
{
|
||||
"get_outstanding_invoices": True,
|
||||
"from_posting_date": add_days(nowdate(), -4),
|
||||
"to_posting_date": add_days(nowdate(), 2),
|
||||
}
|
||||
)
|
||||
references = get_outstanding_reference_documents(args)
|
||||
|
||||
self.assertEqual(len(references), 3)
|
||||
self.assertEqual(references[0].voucher_no, si.name)
|
||||
self.assertEqual(references[1].voucher_no, si2.name)
|
||||
self.assertEqual(references[2].voucher_no, si2.name)
|
||||
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
|
||||
self.assertEqual(references[2].payment_term, "Tax Receivable")
|
||||
|
||||
def test_partial_cancel_for_payment_entry(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
# Additional GL Entry
|
||||
tax_amount = 10
|
||||
reference_row = pe.references[0]
|
||||
gl_args = {
|
||||
"party_type": pe.party_type,
|
||||
"party": pe.party,
|
||||
"against_voucher_type": reference_row.reference_doctype,
|
||||
"against_voucher": reference_row.reference_name,
|
||||
"voucher_detail_no": reference_row.name,
|
||||
}
|
||||
|
||||
gl_dicts = []
|
||||
|
||||
gl_dicts.extend(
|
||||
[
|
||||
pe.get_gl_dict(
|
||||
{
|
||||
"account": pe.paid_to,
|
||||
"credit": tax_amount,
|
||||
"credit_in_account_currency": tax_amount,
|
||||
**gl_args,
|
||||
}
|
||||
),
|
||||
pe.get_gl_dict(
|
||||
{
|
||||
"account": pe.paid_from,
|
||||
"debit": tax_amount,
|
||||
"debit_in_account_currency": tax_amount,
|
||||
**gl_args,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
make_gl_entries(gl_dicts)
|
||||
|
||||
# Assert PLEs Before
|
||||
self.assertPLEntries(
|
||||
pe,
|
||||
[
|
||||
{"amount": -100.0, "against_voucher_no": si.name},
|
||||
{"amount": 10.0, "against_voucher_no": si.name},
|
||||
],
|
||||
)
|
||||
|
||||
# Partially cancel Payment Entry
|
||||
make_reverse_gl_entries(gl_dicts, partial_cancel=True)
|
||||
self.assertPLEntries(pe, [{"amount": -100.0, "against_voucher_no": si.name}])
|
||||
|
||||
def assertPLEntries(self, payment_doc, expected_pl_entries):
|
||||
pl_entries = frappe.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": payment_doc.doctype,
|
||||
"voucher_no": payment_doc.name,
|
||||
"delinked": 0,
|
||||
},
|
||||
fields=["amount", "against_voucher_no"],
|
||||
)
|
||||
out_str = json.dumps(sorted(pl_entries, key=json.dumps))
|
||||
expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps))
|
||||
self.assertEqual(out_str, expected_out_str)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1279,6 +1391,9 @@ def create_payment_terms_template():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
"""
|
||||
Create a Payment Terms Template with % or amount discount.
|
||||
"""
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
|
||||
@@ -203,9 +203,10 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-15 05:35:50.109290",
|
||||
"modified": "2023-11-17 17:33:55.701726",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
@@ -230,6 +231,5 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
"states": []
|
||||
}
|
||||
|
||||
@@ -29,6 +29,58 @@ class PaymentReconciliation(Document):
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
doc_dict = frappe._dict(
|
||||
{
|
||||
"modified": None,
|
||||
"company": None,
|
||||
"party": None,
|
||||
"party_type": None,
|
||||
"receivable_payable_account": None,
|
||||
"default_advance_account": None,
|
||||
"from_invoice_date": None,
|
||||
"to_invoice_date": None,
|
||||
"invoice_limit": 50,
|
||||
"from_payment_date": None,
|
||||
"to_payment_date": None,
|
||||
"payment_limit": 50,
|
||||
"minimum_invoice_amount": None,
|
||||
"minimum_payment_amount": None,
|
||||
"maximum_invoice_amount": None,
|
||||
"maximum_payment_amount": None,
|
||||
"bank_cash_account": None,
|
||||
"cost_center": None,
|
||||
"payment_name": None,
|
||||
"invoice_name": None,
|
||||
}
|
||||
)
|
||||
super(Document, self).__init__(doc_dict)
|
||||
|
||||
def save(self):
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
pass
|
||||
|
||||
def db_insert(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def db_update(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
self.get_nonreconciled_payment_entries()
|
||||
@@ -487,6 +539,27 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
if journals:
|
||||
journals = list(set(journals))
|
||||
journals_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"exchange_rate",
|
||||
],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
|
||||
@@ -1137,6 +1137,40 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(pay.unallocated_amount, 1000)
|
||||
self.assertEqual(pay.difference_amount, 0)
|
||||
|
||||
def test_rounding_of_unallocated_amount(self):
|
||||
self.supplier = "_Test Supplier USD"
|
||||
pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True)
|
||||
pi.supplier = self.supplier
|
||||
pi.currency = "USD"
|
||||
pi.conversion_rate = 80
|
||||
pi.credit_to = self.creditors_usd
|
||||
pi.save().submit()
|
||||
|
||||
pe = get_payment_entry(pi.doctype, pi.name)
|
||||
pe.target_exchange_rate = 78.726500000
|
||||
pe.received_amount = 26.75
|
||||
pe.paid_amount = 2105.93
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
# unallocated_amount will have some rounding loss - 26.749950
|
||||
self.assertNotEqual(pe.unallocated_amount, 26.75)
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -159,9 +159,10 @@
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-23 10:44:56.066303",
|
||||
"modified": "2023-11-17 17:33:38.612615",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationAllocation(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -71,9 +71,10 @@
|
||||
"label": "Exchange Rate"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-08 18:18:02.502149",
|
||||
"modified": "2023-11-17 17:33:45.455166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Invoice",
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationInvoice(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -107,9 +107,10 @@
|
||||
"options": "Cost Center"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-03 07:43:29.965353",
|
||||
"modified": "2023-11-17 17:33:34.818530",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationPayment(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"is_pos",
|
||||
"is_return",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"update_billed_amount_in_delivery_note",
|
||||
"column_break1",
|
||||
"company",
|
||||
"posting_date",
|
||||
@@ -1549,12 +1550,19 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-30 03:49:50.455199",
|
||||
"modified": "2023-11-20 12:27:12.848149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -659,7 +659,7 @@ def get_stock_availability(item_code, warehouse):
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", item_code):
|
||||
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -821,7 +822,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-02 12:52:39.125295",
|
||||
"modified": "2023-11-14 18:33:22.585715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
@@ -831,4 +832,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -181,7 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
unblock_invoice() {
|
||||
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
@@ -494,6 +495,7 @@ class PurchaseInvoice(BuyingController):
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def on_submit(self):
|
||||
@@ -532,7 +534,11 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.update_project()
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
|
||||
):
|
||||
self.update_project()
|
||||
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
self.update_advance_tax_references()
|
||||
|
||||
@@ -1269,7 +1275,10 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.update_project()
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
|
||||
):
|
||||
self.update_project()
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
@@ -1281,19 +1290,29 @@ class PurchaseInvoice(BuyingController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
def update_project(self):
|
||||
project_list = []
|
||||
projects = frappe._dict()
|
||||
for d in self.items:
|
||||
if d.project and d.project not in project_list:
|
||||
project = frappe.get_doc("Project", d.project)
|
||||
project.update_purchase_costing()
|
||||
project.db_update()
|
||||
project_list.append(d.project)
|
||||
if d.project:
|
||||
if self.docstatus == 1:
|
||||
projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
|
||||
elif self.docstatus == 2:
|
||||
projects[d.project] = projects.get(d.project, 0) - d.base_net_amount
|
||||
|
||||
pj = frappe.qb.DocType("Project")
|
||||
for proj, value in projects.items():
|
||||
res = (
|
||||
frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
||||
)
|
||||
current_purchase_cost = res and res[0][0] or 0
|
||||
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
@@ -1686,6 +1705,4 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.set_onload("ignore_price_list", True)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -285,6 +286,7 @@
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -491,6 +493,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
@@ -498,6 +501,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": ":Company",
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "cost_center",
|
||||
@@ -890,7 +894,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-03 21:01:01.824892",
|
||||
"modified": "2023-11-30 16:26:05.629780",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -10,12 +10,7 @@ from frappe.utils.data import comma_and
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
self._allowed_types = get_allowed_types_from_settings()
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
@@ -56,15 +51,7 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
def validate_vouchers(self):
|
||||
if self.vouchers:
|
||||
# Validate voucher types
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
if disallowed_types := voucher_types.difference(self._allowed_types):
|
||||
frappe.throw(
|
||||
_("{0} types are not allowed. Only {1} are.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
frappe.bold(comma_and(list(self._allowed_types))),
|
||||
)
|
||||
)
|
||||
validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers])
|
||||
|
||||
def get_existing_ledger_entries(self):
|
||||
vouchers = [x.voucher_no for x in self.vouchers]
|
||||
@@ -121,7 +108,7 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
if len(self.vouchers) > 5:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
@@ -165,7 +152,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
@@ -191,6 +185,25 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
)
|
||||
|
||||
|
||||
def validate_docs_for_voucher_types(doc_voucher_types):
|
||||
allowed_types = get_allowed_types_from_settings()
|
||||
# Validate voucher types
|
||||
voucher_types = set(doc_voucher_types)
|
||||
if disallowed_types := voucher_types.difference(allowed_types):
|
||||
message = "are" if len(disallowed_types) > 1 else "is"
|
||||
frappe.throw(
|
||||
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
message,
|
||||
frappe.bold(
|
||||
frappe.utils.get_link_to_form(
|
||||
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
@@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
@@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@@ -178,10 +178,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
@@ -557,15 +556,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
|
||||
}
|
||||
}
|
||||
|
||||
// Income Account in Details Table
|
||||
// --------------------------------
|
||||
cur_frm.set_query("income_account", "items", function(doc) {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: {'company': doc.company}
|
||||
}
|
||||
});
|
||||
|
||||
// Cost Center in Details Table
|
||||
// -----------------------------
|
||||
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
|
||||
@@ -660,6 +650,16 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("income_account", "items", function() {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: {
|
||||
'company': frm.doc.company,
|
||||
"disabled": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Delivery Note': 'Delivery',
|
||||
'Sales Invoice': 'Return / Credit Note',
|
||||
|
||||
@@ -1613,7 +1613,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Inter Company Invoice Reference",
|
||||
"options": "Purchase Invoice",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
@@ -2147,7 +2148,7 @@
|
||||
"label": "Use Company default Cost Center for Round off"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
@@ -2164,7 +2165,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-03 14:39:38.012346",
|
||||
"modified": "2023-11-23 16:56:29.679499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -17,6 +17,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
@@ -180,6 +181,7 @@ class SalesInvoice(SellingController):
|
||||
self.validate_write_off_account()
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
validate_docs_for_voucher_types(["Sales Invoice"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
@@ -408,7 +410,7 @@ class SalesInvoice(SellingController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
@@ -1932,7 +1934,6 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
doclist.set_onload("ignore_price_list", True)
|
||||
return doclist
|
||||
|
||||
|
||||
|
||||
@@ -782,6 +782,28 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
w = self.make()
|
||||
self.assertEqual(w.outstanding_amount, w.base_rounded_total)
|
||||
|
||||
def test_rounded_total_with_cash_discount(self):
|
||||
si = frappe.copy_doc(test_records[2])
|
||||
|
||||
item = copy.deepcopy(si.get("items")[0])
|
||||
item.update(
|
||||
{
|
||||
"qty": 1,
|
||||
"rate": 14960.66,
|
||||
}
|
||||
)
|
||||
|
||||
si.set("items", [item])
|
||||
si.set("taxes", [])
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.is_cash_or_non_trade_discount = 1
|
||||
si.discount_amount = 1
|
||||
si.insert()
|
||||
|
||||
self.assertEqual(si.grand_total, 14959.66)
|
||||
self.assertEqual(si.rounded_total, 14960)
|
||||
self.assertEqual(si.rounding_adjustment, 0.34)
|
||||
|
||||
def test_payment(self):
|
||||
w = self.make()
|
||||
|
||||
@@ -2769,6 +2791,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -891,7 +892,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-25 11:58:10.723833",
|
||||
"modified": "2023-11-14 18:34:10.479329",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -901,4 +902,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
@@ -73,7 +73,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
@@ -138,7 +138,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
@@ -196,7 +196,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
@@ -281,7 +281,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Unreconcile Payments", {
|
||||
frappe.ui.form.on("Unreconcile Payment", {
|
||||
refresh(frm) {
|
||||
frm.set_query("voucher_type", function() {
|
||||
return {
|
||||
@@ -21,7 +21,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Unreconcile Payments",
|
||||
"options": "Unreconcile Payment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -61,7 +61,7 @@
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payments",
|
||||
"name": "Unreconcile Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@@ -90,4 +90,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.utils import (
|
||||
)
|
||||
|
||||
|
||||
class UnreconcilePayments(Document):
|
||||
class UnreconcilePayment(Document):
|
||||
def validate(self):
|
||||
self.supported_types = ["Payment Entry", "Journal Entry"]
|
||||
if not self.voucher_type in self.supported_types:
|
||||
@@ -142,7 +142,7 @@ def create_unreconcile_doc_for_selection(selections=None):
|
||||
selections = frappe.json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payments")
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
unrecon.company = row.get("company")
|
||||
unrecon.voucher_type = row.get("voucher_type")
|
||||
unrecon.voucher_no = row.get("voucher_no")
|
||||
@@ -556,7 +556,12 @@ def get_round_off_account_and_cost_center(
|
||||
|
||||
|
||||
def make_reverse_gl_entries(
|
||||
gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes"
|
||||
gl_entries=None,
|
||||
voucher_type=None,
|
||||
voucher_no=None,
|
||||
adv_adj=False,
|
||||
update_outstanding="Yes",
|
||||
partial_cancel=False,
|
||||
):
|
||||
"""
|
||||
Get original gl entries of the voucher
|
||||
@@ -576,14 +581,19 @@ def make_reverse_gl_entries(
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(
|
||||
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
|
||||
gl_entries,
|
||||
cancel=1,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
partial_cancel=partial_cancel,
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
if not partial_cancel:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
|
||||
@@ -31,7 +31,12 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
|
||||
PURCHASE_TRANSACTION_TYPES = {
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
}
|
||||
SALES_TRANSACTION_TYPES = {
|
||||
"Quotation",
|
||||
"Sales Order",
|
||||
@@ -190,7 +195,7 @@ def set_address_details(
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
*,
|
||||
ignore_permissions=False
|
||||
ignore_permissions=False,
|
||||
):
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
@@ -231,8 +236,10 @@ def set_address_details(
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
shipping_address_display=render_address(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||
shipping_address_display=render_address(
|
||||
shipping_address, check_permissions=not ignore_permissions
|
||||
),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address),
|
||||
)
|
||||
|
||||
if party_details.company_address:
|
||||
@@ -243,7 +250,7 @@ def set_address_details(
|
||||
party_details.company_address_display
|
||||
or render_address(party_details.company_address, check_permissions=False)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address),
|
||||
)
|
||||
|
||||
# shipping address - if not already set
|
||||
@@ -251,7 +258,7 @@ def set_address_details(
|
||||
party_details.update(
|
||||
shipping_address=party_details.billing_address,
|
||||
shipping_address_display=party_details.billing_address_display,
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
|
||||
)
|
||||
|
||||
party_address, shipping_address = (
|
||||
@@ -949,6 +956,9 @@ def get_partywise_advanced_payment_amount(
|
||||
if party:
|
||||
query = query.where(ple.party == party)
|
||||
|
||||
if invoice_doctypes := frappe.get_hooks("invoice_doctypes"):
|
||||
query = query.where(ple.voucher_type.notin(invoice_doctypes))
|
||||
|
||||
data = query.run()
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
@@ -144,6 +144,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "for_revaluation_journals",
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
|
||||
@@ -110,6 +110,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
"fieldname":"based_on_payment_terms",
|
||||
"label": __("Based On Payment Terms"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "for_revaluation_journals",
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -114,10 +114,13 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldname":"customer_group",
|
||||
"label": __("Customer Group"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group"
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Customer Group",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Customer Group', txt);
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms_template",
|
||||
@@ -173,12 +176,18 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "for_revaluation_journals",
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -281,11 +281,20 @@ class ReceivablePayableReport(object):
|
||||
|
||||
row.invoice_grand_total = row.invoiced
|
||||
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
(abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision)
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
|
||||
if must_consider:
|
||||
# non-zero oustanding, we must consider this row
|
||||
|
||||
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
||||
@@ -753,7 +762,12 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
|
||||
if self.filters.get("show_remarks"):
|
||||
query = query.select(ple.remarks)
|
||||
if remarks_length := frappe.db.get_single_value(
|
||||
"Accounts Settings", "receivable_payable_remarks_length"
|
||||
):
|
||||
query = query.select(Substring(ple.remarks, 1, remarks_length).as_("remarks"))
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
@@ -840,7 +854,13 @@ class ReceivablePayableReport(object):
|
||||
self.customer = qb.DocType("Customer")
|
||||
|
||||
if self.filters.get("customer_group"):
|
||||
self.get_hierarchical_filters("Customer Group", "customer_group")
|
||||
groups = get_customer_group_with_children(self.filters.customer_group)
|
||||
customers = (
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer["customer_group"].isin(groups))
|
||||
)
|
||||
self.qb_selection_filter.append(self.ple.party.isin(customers))
|
||||
|
||||
if self.filters.get("territory"):
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
@@ -1065,7 +1085,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
|
||||
if self.filters.show_remarks:
|
||||
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
|
||||
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200)
|
||||
|
||||
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
|
||||
if not fieldname:
|
||||
@@ -1132,3 +1152,19 @@ class ReceivablePayableReport(object):
|
||||
.run()
|
||||
)
|
||||
self.err_journals = [x[0] for x in results] if results else []
|
||||
|
||||
|
||||
def get_customer_group_with_children(customer_groups):
|
||||
if not isinstance(customer_groups, list):
|
||||
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
|
||||
|
||||
all_customer_groups = []
|
||||
for d in customer_groups:
|
||||
if frappe.db.exists("Customer Group", d):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_customer_groups += [c.name for c in children]
|
||||
else:
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
@@ -475,6 +475,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 0)
|
||||
|
||||
def test_multi_customer_group_filter(self):
|
||||
si = self.create_sales_invoice()
|
||||
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
|
||||
# Create a list of customer groups, e.g., ["Group1", "Group2"]
|
||||
cus_groups_list = [cus_group, "_Test Customer Group 1"]
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer_group": cus_groups_list, # Use the list of customer groups
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
|
||||
# Assert that the report contains data for the specified customer groups
|
||||
self.assertTrue(len(report) > 0)
|
||||
|
||||
for row in report:
|
||||
# Assert that the customer group of each row is in the list of customer groups
|
||||
self.assertIn(row.customer_group, cus_groups_list)
|
||||
|
||||
def test_party_account_filter(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
self.customer2 = (
|
||||
|
||||
@@ -139,6 +139,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
"label": __("Show GL Balance"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "for_revaluation_journals",
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
],
|
||||
|
||||
onload: function(report) {
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -35,6 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def get_data(self, args):
|
||||
self.data = []
|
||||
self.receivables = ReceivablePayableReport(self.filters).run(args)[1]
|
||||
self.currency_precision = get_currency_precision() or 2
|
||||
|
||||
self.get_party_total(args)
|
||||
|
||||
@@ -58,7 +60,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if party_dict.outstanding == 0:
|
||||
if flt(party_dict.outstanding, self.currency_precision) == 0:
|
||||
continue
|
||||
|
||||
row = frappe._dict()
|
||||
|
||||
@@ -8,7 +8,17 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
getdate,
|
||||
today,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -43,6 +53,8 @@ def get_period_list(
|
||||
year_start_date = getdate(period_start_date)
|
||||
year_end_date = getdate(period_end_date)
|
||||
|
||||
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
|
||||
|
||||
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
|
||||
|
||||
period_list = []
|
||||
|
||||
@@ -164,7 +164,12 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
credit_in_account_currency """
|
||||
|
||||
if filters.get("show_remarks"):
|
||||
select_fields += """,remarks"""
|
||||
if remarks_length := frappe.db.get_single_value(
|
||||
"Accounts Settings", "general_ledger_remarks_length"
|
||||
):
|
||||
select_fields += f",substr(remarks, 1, {remarks_length}) as 'remarks'"
|
||||
else:
|
||||
select_fields += """,remarks"""
|
||||
|
||||
order_by_statement = "order by posting_date, account, creation"
|
||||
|
||||
@@ -277,7 +282,8 @@ def get_conditions(filters):
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if not dimension.disabled:
|
||||
# Ignore 'Finance Book' set up as dimension in below logic, as it is already handled in above section
|
||||
if not dimension.disabled and dimension.document_type != "Finance Book":
|
||||
if filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
filters[dimension.fieldname] = get_dimension_with_children(
|
||||
|
||||
@@ -184,6 +184,16 @@ def get_columns(filters):
|
||||
"width": 180,
|
||||
}
|
||||
)
|
||||
else:
|
||||
columns.append(
|
||||
{
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
|
||||
@@ -179,6 +179,7 @@ def get_balance_on(
|
||||
in_account_currency=True,
|
||||
cost_center=None,
|
||||
ignore_account_permission=False,
|
||||
start_date=None,
|
||||
):
|
||||
if not account and frappe.form_dict.get("account"):
|
||||
account = frappe.form_dict.get("account")
|
||||
@@ -192,6 +193,8 @@ def get_balance_on(
|
||||
cost_center = frappe.form_dict.get("cost_center")
|
||||
|
||||
cond = ["is_cancelled=0"]
|
||||
if start_date:
|
||||
cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date)))
|
||||
if date:
|
||||
cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
|
||||
else:
|
||||
@@ -533,7 +536,7 @@ def check_if_advance_entry_modified(args):
|
||||
where
|
||||
name = %(voucher_no)s and docstatus = 1
|
||||
and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s
|
||||
and round(unallocated_amount, {1}) = %(unreconciled_amount)s
|
||||
and round(unallocated_amount, {1}) = round(%(unreconciled_amount)s, {1})
|
||||
""".format(
|
||||
party_account_field, precision
|
||||
),
|
||||
@@ -1617,6 +1620,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
due_date=gle.due_date,
|
||||
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,
|
||||
@@ -1638,7 +1642,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
|
||||
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False
|
||||
):
|
||||
if gl_entries:
|
||||
ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel)
|
||||
@@ -1648,7 +1652,7 @@ def create_payment_ledger_entry(
|
||||
ple = frappe.get_doc(entry)
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
delink_original_entry(ple, partial_cancel=partial_cancel)
|
||||
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.flags.adv_adj = adv_adj
|
||||
@@ -1695,7 +1699,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry):
|
||||
def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
if pl_entry:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
@@ -1715,6 +1719,10 @@ def delink_original_entry(pl_entry):
|
||||
& (ple.against_voucher_no == pl_entry.against_voucher_no)
|
||||
)
|
||||
)
|
||||
|
||||
if partial_cancel:
|
||||
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
|
||||
|
||||
query.run()
|
||||
|
||||
|
||||
@@ -1791,6 +1799,30 @@ class QueryPaymentLedger(object):
|
||||
Table("outstanding").amount_in_account_currency >= self.max_outstanding
|
||||
)
|
||||
|
||||
if self.limit and self.get_invoices:
|
||||
outstanding_vouchers = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_against_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.where(Criterion.all(self.dimensions_filter))
|
||||
.where(Criterion.all(self.voucher_posting_date))
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||
.orderby(ple.posting_date, ple.voucher_no)
|
||||
.having(qb.Field("amount_in_account_currency") > 0)
|
||||
.limit(self.limit)
|
||||
.run()
|
||||
)
|
||||
if outstanding_vouchers:
|
||||
filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers]))
|
||||
filter_on_against_voucher_no.append(
|
||||
ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers])
|
||||
)
|
||||
|
||||
# build query for voucher amount
|
||||
query_voucher_amount = (
|
||||
qb.from_(ple)
|
||||
|
||||
@@ -322,13 +322,16 @@ frappe.ui.form.on('Asset', {
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
if (frm.doc.finance_books) {
|
||||
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
|
||||
if (frm.doc.finance_books.length) {
|
||||
var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
|
||||
? true : false;
|
||||
var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0
|
||||
? true : false;
|
||||
|
||||
frm.toggle_enable("schedules", is_editable);
|
||||
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable);
|
||||
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable);
|
||||
frm.toggle_enable("schedules", is_manual_hence_editable || is_shift_hence_editable);
|
||||
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_manual_hence_editable);
|
||||
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable);
|
||||
frm.fields_dict["schedules"].grid.toggle_enable("shift", is_shift_hence_editable);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -495,11 +495,11 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval.doc.asset_quantity",
|
||||
"default": "1",
|
||||
"fieldname": "asset_quantity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Asset Quantity",
|
||||
"read_only": 1
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
@@ -565,7 +565,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2023-10-27 17:03:46.629617",
|
||||
"modified": "2023-11-20 21:05:45.216899",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -43,6 +43,7 @@ class Asset(AccountsController):
|
||||
self.validate_finance_books()
|
||||
if not self.split_from:
|
||||
self.prepare_depreciation_data()
|
||||
self.update_shift_depr_schedule()
|
||||
self.validate_gross_and_purchase_amount()
|
||||
if self.get("schedules"):
|
||||
self.validate_expected_value_after_useful_life()
|
||||
@@ -104,6 +105,13 @@ class Asset(AccountsController):
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
def update_shift_depr_schedule(self):
|
||||
if not any(fb.get("shift_based") for fb in self.finance_books) or self.docstatus != 0:
|
||||
return
|
||||
|
||||
self.make_depreciation_schedule()
|
||||
self.set_accumulated_depreciation()
|
||||
|
||||
def should_prepare_depreciation_schedule(self):
|
||||
if not self.get("schedules"):
|
||||
return True
|
||||
@@ -318,7 +326,7 @@ class Asset(AccountsController):
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None):
|
||||
def make_depreciation_schedule(self, date_of_disposal=None, value_after_depreciation=None):
|
||||
if not self.get("schedules"):
|
||||
self.schedules = []
|
||||
|
||||
@@ -410,13 +418,7 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self._add_depreciation_row(
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
finance_book.depreciation_method,
|
||||
finance_book.finance_book,
|
||||
finance_book.idx,
|
||||
)
|
||||
self._add_depreciation_row(date_of_disposal, depreciation_amount, finance_book, n)
|
||||
|
||||
break
|
||||
|
||||
@@ -425,6 +427,10 @@ class Asset(AccountsController):
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
self, value_after_depreciation, finance_book, False
|
||||
)
|
||||
== finance_book.rate_of_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
self.available_for_use_date, -1
|
||||
@@ -498,25 +504,27 @@ class Asset(AccountsController):
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0:
|
||||
self._add_depreciation_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
finance_book.depreciation_method,
|
||||
finance_book.finance_book,
|
||||
finance_book.idx,
|
||||
)
|
||||
self._add_depreciation_row(schedule_date, depreciation_amount, finance_book, n)
|
||||
|
||||
def _add_depreciation_row(self, schedule_date, depreciation_amount, finance_book, schedule_idx):
|
||||
if finance_book.shift_based:
|
||||
shift = (
|
||||
self.schedules_before_clearing[schedule_idx].shift
|
||||
if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx
|
||||
else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name")
|
||||
)
|
||||
else:
|
||||
shift = None
|
||||
|
||||
def _add_depreciation_row(
|
||||
self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id
|
||||
):
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": depreciation_method,
|
||||
"finance_book": finance_book,
|
||||
"finance_book_id": finance_book_id,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
"shift": shift,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -545,6 +553,8 @@ class Asset(AccountsController):
|
||||
num_of_depreciations_completed = 0
|
||||
depr_schedule = []
|
||||
|
||||
self.schedules_before_clearing = self.get("schedules")
|
||||
|
||||
for schedule in self.get("schedules"):
|
||||
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 2):
|
||||
@@ -752,10 +762,12 @@ class Asset(AccountsController):
|
||||
and not date_of_return
|
||||
):
|
||||
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
||||
depreciation_amount += flt(
|
||||
value_after_depreciation - flt(book.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
|
||||
if not book.shift_based:
|
||||
depreciation_amount += flt(
|
||||
value_after_depreciation - flt(book.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
|
||||
d.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation += d.depreciation_amount
|
||||
@@ -1217,10 +1229,12 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
"total_number_of_depreciations": d.total_number_of_depreciations,
|
||||
"frequency_of_depreciation": d.frequency_of_depreciation,
|
||||
"daily_prorata_based": d.daily_prorata_based,
|
||||
"shift_based": d.shift_based,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
"depreciation_start_date": d.depreciation_start_date or nowdate(),
|
||||
"rate_of_depreciation": d.rate_of_depreciation,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1377,13 +1391,18 @@ def get_depreciation_amount(
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row, show_msg=True
|
||||
):
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
def get_straight_line_or_manual_depr_amount(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations
|
||||
):
|
||||
if row.shift_based:
|
||||
return get_shift_depr_amount(asset, row, schedule_idx)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
|
||||
@@ -1478,6 +1497,40 @@ def get_straight_line_or_manual_depr_amount(
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_shift_depr_amount(asset, row, schedule_idx):
|
||||
if asset.get("__islocal") and not asset.flags.shift_allocation:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
shift = (
|
||||
asset.schedules_before_clearing[schedule_idx].shift
|
||||
if len(asset.schedules_before_clearing) > schedule_idx
|
||||
else None
|
||||
)
|
||||
shift_factor = asset_shift_factors_map.get(shift) if shift else 0
|
||||
|
||||
shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift)) for schedule in asset.schedules_before_clearing
|
||||
)
|
||||
|
||||
return (
|
||||
(
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
/ flt(shift_factors_sum)
|
||||
) * shift_factor
|
||||
|
||||
|
||||
def get_asset_shift_factors_map():
|
||||
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
|
||||
@@ -464,6 +464,9 @@ def restore_asset(asset_name):
|
||||
|
||||
|
||||
def depreciate_asset(asset, date):
|
||||
if not asset.calculate_depreciation:
|
||||
return
|
||||
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_disposal=date)
|
||||
asset.save()
|
||||
@@ -472,6 +475,9 @@ def depreciate_asset(asset, date):
|
||||
|
||||
|
||||
def reset_depreciation_schedule(asset, date):
|
||||
if not asset.calculate_depreciation:
|
||||
return
|
||||
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
|
||||
@@ -142,12 +142,7 @@ class TestAsset(AssetSetup):
|
||||
("Creditors - _TC", 0.0, 100000.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
pi.name,
|
||||
)
|
||||
gle = get_gl_entries("Purchase Invoice", pi.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
pi.cancel()
|
||||
@@ -249,12 +244,7 @@ class TestAsset(AssetSetup):
|
||||
),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_no = %s
|
||||
order by account""",
|
||||
asset.journal_entry_for_scrap,
|
||||
)
|
||||
gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
restore_asset(asset.name)
|
||||
@@ -316,13 +306,7 @@ class TestAsset(AssetSetup):
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.cancel()
|
||||
@@ -392,13 +376,7 @@ class TestAsset(AssetSetup):
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
@@ -526,13 +504,7 @@ class TestAsset(AssetSetup):
|
||||
("CWIP Account - _TC", 5250.0, 0.0),
|
||||
)
|
||||
|
||||
pr_gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Receipt' and voucher_no = %s
|
||||
order by account""",
|
||||
pr.name,
|
||||
)
|
||||
|
||||
pr_gle = get_gl_entries("Purchase Receipt", pr.name)
|
||||
self.assertSequenceEqual(pr_gle, expected_gle)
|
||||
|
||||
pi = make_invoice(pr.name)
|
||||
@@ -545,13 +517,7 @@ class TestAsset(AssetSetup):
|
||||
("Creditors - _TC", 0.0, 5500.0),
|
||||
)
|
||||
|
||||
pi_gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
pi.name,
|
||||
)
|
||||
|
||||
pi_gle = get_gl_entries("Purchase Invoice", pi.name)
|
||||
self.assertSequenceEqual(pi_gle, expected_gle)
|
||||
|
||||
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
|
||||
@@ -578,13 +544,7 @@ class TestAsset(AssetSetup):
|
||||
|
||||
expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0))
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Asset' and voucher_no = %s
|
||||
order by account""",
|
||||
asset_doc.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_cwip_toggling_cases(self):
|
||||
@@ -607,10 +567,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 1 -- PR with cwip disabled, Asset with cwip enabled
|
||||
@@ -624,10 +581,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 2 -- PR with cwip enabled, Asset with cwip disabled
|
||||
@@ -640,10 +594,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertTrue(gle)
|
||||
|
||||
# case 3 -- PI with cwip disabled, Asset with cwip enabled
|
||||
@@ -656,10 +607,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 4 -- PI with cwip enabled, Asset with cwip disabled
|
||||
@@ -672,10 +620,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertTrue(gle)
|
||||
|
||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
|
||||
@@ -1644,6 +1589,30 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv.insert)
|
||||
|
||||
def test_multi_currency_asset_pr_creation(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro",
|
||||
qty=1,
|
||||
rate=100.0,
|
||||
location="Test Location",
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
||||
|
||||
|
||||
def get_gl_entries(doctype, docname):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
return (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(gl_entry.account, gl_entry.debit, gl_entry.credit)
|
||||
.where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname))
|
||||
.orderby(gl_entry.account)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
@@ -1706,6 +1675,7 @@ def create_asset(**args):
|
||||
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||
"shift_based": args.shift_based or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"depreciation_start_date",
|
||||
@@ -93,12 +94,19 @@
|
||||
"fieldname": "daily_prorata_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on daily pro-rata"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\"",
|
||||
"fieldname": "shift_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 22:21:52.090191",
|
||||
"modified": "2023-11-29 03:53:03.591098",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Asset Shift Allocation', {
|
||||
onload: function(frm) {
|
||||
frm.events.make_schedules_editable(frm);
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
frm.toggle_enable("depreciation_schedule", true);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2023-11-29 04:01:33.796458",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_esaa",
|
||||
"asset",
|
||||
"naming_series",
|
||||
"column_break_tdae",
|
||||
"finance_book",
|
||||
"amended_from",
|
||||
"depreciation_schedule_section",
|
||||
"depreciation_schedule"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_esaa",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Naming Series",
|
||||
"options": "ACC-ASA-.YYYY.-",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tdae",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "depreciation_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Depreciation Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "depreciation_schedule",
|
||||
"fieldtype": "Table",
|
||||
"label": "Depreciation Schedule",
|
||||
"options": "Depreciation Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Asset Shift Allocation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 04:06:20.586168",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Shift Allocation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_months, cint, flt, get_last_day
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_shift_factors_map
|
||||
from erpnext.assets.doctype.asset.depreciation import is_last_day_of_the_month
|
||||
|
||||
|
||||
class AssetShiftAllocation(Document):
|
||||
def after_insert(self):
|
||||
self.fetch_and_set_depr_schedule()
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
self.validate_invalid_shift_change()
|
||||
self.update_depr_schedule()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_asset_schedule()
|
||||
|
||||
def fetch_and_set_depr_schedule(self):
|
||||
if len(self.asset_doc.finance_books) != 1:
|
||||
frappe.throw(_("Only assets with one finance book allowed in v14."))
|
||||
|
||||
if not any(fb.get("shift_based") for fb in self.asset_doc.finance_books):
|
||||
frappe.throw(_("Asset {0} is not using shift based depreciation").format(self.asset))
|
||||
|
||||
for schedule in self.asset_doc.get("schedules"):
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
"depreciation_method": self.asset_doc.finance_books[0].depreciation_method,
|
||||
"finance_book": self.asset_doc.finance_books[0].finance_book,
|
||||
"finance_book_id": self.asset_doc.finance_books[0].idx,
|
||||
},
|
||||
)
|
||||
|
||||
self.flags.ignore_validate = True
|
||||
self.save()
|
||||
|
||||
def validate_invalid_shift_change(self):
|
||||
if not self.get("depreciation_schedule") or self.docstatus == 1:
|
||||
return
|
||||
|
||||
for i, sch in enumerate(self.depreciation_schedule):
|
||||
if sch.journal_entry and self.asset_doc.schedules[i].shift != sch.shift:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Shift cannot be changed since the depreciation has already been processed"
|
||||
).format(i)
|
||||
)
|
||||
|
||||
def update_depr_schedule(self):
|
||||
if not self.get("depreciation_schedule") or self.docstatus == 1:
|
||||
return
|
||||
|
||||
self.allocate_shift_diff_in_depr_schedule()
|
||||
|
||||
temp_asset_doc = frappe.copy_doc(self.asset_doc)
|
||||
|
||||
temp_asset_doc.flags.shift_allocation = True
|
||||
|
||||
temp_asset_doc.schedules = []
|
||||
|
||||
for schedule in self.depreciation_schedule:
|
||||
temp_asset_doc.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
"depreciation_method": self.asset_doc.finance_books[0].depreciation_method,
|
||||
"finance_book": self.asset_doc.finance_books[0].finance_book,
|
||||
"finance_book_id": self.asset_doc.finance_books[0].idx,
|
||||
},
|
||||
)
|
||||
|
||||
temp_asset_doc.prepare_depreciation_data()
|
||||
|
||||
self.depreciation_schedule = []
|
||||
|
||||
for schedule in temp_asset_doc.get("schedules"):
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
"depreciation_method": self.asset_doc.finance_books[0].depreciation_method,
|
||||
"finance_book": self.asset_doc.finance_books[0].finance_book,
|
||||
"finance_book_id": self.asset_doc.finance_books[0].idx,
|
||||
},
|
||||
)
|
||||
|
||||
def allocate_shift_diff_in_depr_schedule(self):
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
reverse_asset_shift_factors_map = {
|
||||
asset_shift_factors_map[k]: k for k in asset_shift_factors_map
|
||||
}
|
||||
|
||||
original_shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.asset_doc.schedules
|
||||
)
|
||||
|
||||
new_shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule
|
||||
)
|
||||
|
||||
diff = new_shift_factors_sum - original_shift_factors_sum
|
||||
|
||||
if diff > 0:
|
||||
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
|
||||
if diff <= 0:
|
||||
break
|
||||
|
||||
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
|
||||
|
||||
if shift_factor <= diff:
|
||||
self.depreciation_schedule.pop()
|
||||
diff -= shift_factor
|
||||
else:
|
||||
try:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
|
||||
shift_factor - diff
|
||||
)
|
||||
diff = 0
|
||||
except Exception:
|
||||
frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
|
||||
shift_factor - diff
|
||||
)
|
||||
elif diff < 0:
|
||||
shift_factors = list(asset_shift_factors_map.values())
|
||||
desc_shift_factors = sorted(shift_factors, reverse=True)
|
||||
depr_schedule_len_diff = self.asset_doc.total_number_of_depreciations - len(
|
||||
self.depreciation_schedule
|
||||
)
|
||||
subsets_result = []
|
||||
|
||||
if depr_schedule_len_diff > 0:
|
||||
num_rows_to_add = depr_schedule_len_diff
|
||||
|
||||
while not subsets_result and num_rows_to_add > 0:
|
||||
find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result)
|
||||
if subsets_result:
|
||||
break
|
||||
num_rows_to_add -= 1
|
||||
|
||||
if subsets_result:
|
||||
for i in range(num_rows_to_add):
|
||||
schedule_date = add_months(
|
||||
self.depreciation_schedule[-1].schedule_date,
|
||||
cint(self.asset_doc.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date):
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]),
|
||||
"depreciation_method": self.asset_doc.finance_books[0].depreciation_method,
|
||||
"finance_book": self.asset_doc.finance_books[0].finance_book,
|
||||
"finance_book_id": self.asset_doc.finance_books[0].idx,
|
||||
},
|
||||
)
|
||||
|
||||
if depr_schedule_len_diff <= 0 or not subsets_result:
|
||||
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
|
||||
diff = abs(diff)
|
||||
|
||||
if diff <= 0:
|
||||
break
|
||||
|
||||
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
|
||||
|
||||
if shift_factor <= diff:
|
||||
for sf in desc_shift_factors:
|
||||
if sf - shift_factor <= diff:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf)
|
||||
diff -= sf - shift_factor
|
||||
break
|
||||
else:
|
||||
try:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
|
||||
shift_factor + diff
|
||||
)
|
||||
diff = 0
|
||||
except Exception:
|
||||
frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
|
||||
shift_factor + diff
|
||||
)
|
||||
|
||||
def update_asset_schedule(self):
|
||||
self.asset_doc.flags.shift_allocation = True
|
||||
|
||||
self.asset_doc.schedules = []
|
||||
|
||||
for schedule in self.depreciation_schedule:
|
||||
self.asset_doc.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
"depreciation_method": self.asset_doc.finance_books[0].depreciation_method,
|
||||
"finance_book": self.asset_doc.finance_books[0].finance_book,
|
||||
"finance_book_id": self.asset_doc.finance_books[0].idx,
|
||||
},
|
||||
)
|
||||
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
self.asset_doc.prepare_depreciation_data()
|
||||
self.asset_doc.save()
|
||||
|
||||
|
||||
def find_subsets_with_sum(numbers, k, target_sum, current_subset, result):
|
||||
if k == 0 and target_sum == 0:
|
||||
result.append(current_subset.copy())
|
||||
return
|
||||
if k <= 0 or target_sum <= 0 or not numbers:
|
||||
return
|
||||
|
||||
# Include the current number in the subset
|
||||
find_subsets_with_sum(
|
||||
numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result
|
||||
)
|
||||
|
||||
# Exclude the current number from the subset
|
||||
find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result)
|
||||
@@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset
|
||||
|
||||
|
||||
class TestAssetShiftAllocation(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_asset_shift_factors()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_asset_shift_allocation(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=120000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
shift_based=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 10000.0, 10000.0, "Single"],
|
||||
["2023-02-28", 10000.0, 20000.0, "Single"],
|
||||
["2023-03-31", 10000.0, 30000.0, "Single"],
|
||||
["2023-04-30", 10000.0, 40000.0, "Single"],
|
||||
["2023-05-31", 10000.0, 50000.0, "Single"],
|
||||
["2023-06-30", 10000.0, 60000.0, "Single"],
|
||||
["2023-07-31", 10000.0, 70000.0, "Single"],
|
||||
["2023-08-31", 10000.0, 80000.0, "Single"],
|
||||
["2023-09-30", 10000.0, 90000.0, "Single"],
|
||||
["2023-10-31", 10000.0, 100000.0, "Single"],
|
||||
["2023-11-30", 10000.0, 110000.0, "Single"],
|
||||
["2023-12-31", 10000.0, 120000.0, "Single"],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation = frappe.get_doc(
|
||||
{"doctype": "Asset Shift Allocation", "asset": asset.name}
|
||||
).insert()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset_shift_allocation.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name)
|
||||
asset_shift_allocation.depreciation_schedule[4].shift = "Triple"
|
||||
asset_shift_allocation.save()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset_shift_allocation.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 10000.0, 10000.0, "Single"],
|
||||
["2023-02-28", 10000.0, 20000.0, "Single"],
|
||||
["2023-03-31", 10000.0, 30000.0, "Single"],
|
||||
["2023-04-30", 10000.0, 40000.0, "Single"],
|
||||
["2023-05-31", 20000.0, 60000.0, "Triple"],
|
||||
["2023-06-30", 10000.0, 70000.0, "Single"],
|
||||
["2023-07-31", 10000.0, 80000.0, "Single"],
|
||||
["2023-08-31", 10000.0, 90000.0, "Single"],
|
||||
["2023-09-30", 10000.0, 100000.0, "Single"],
|
||||
["2023-10-31", 10000.0, 110000.0, "Single"],
|
||||
["2023-11-30", 10000.0, 120000.0, "Single"],
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation.submit()
|
||||
|
||||
asset.reload()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
|
||||
def create_asset_shift_factors():
|
||||
shifts = [
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0},
|
||||
]
|
||||
|
||||
for s in shifts:
|
||||
frappe.get_doc(s).insert()
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Asset Shift Factor', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:shift_name",
|
||||
"creation": "2023-11-29 03:45:13.247372",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"shift_name",
|
||||
"shift_factor",
|
||||
"default"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "shift_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shift_factor",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Factor",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Default"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 04:06:31.723038",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Shift Factor",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetShiftFactor(Document):
|
||||
def validate(self):
|
||||
self.validate_default()
|
||||
|
||||
def validate_default(self):
|
||||
if self.default:
|
||||
existing_default_shift_factor = frappe.db.get_value(
|
||||
"Asset Shift Factor", {"default": 1}, "name"
|
||||
)
|
||||
|
||||
if existing_default_shift_factor:
|
||||
frappe.throw(
|
||||
_("Asset Shift Factor {0} is set as default currently. Please change it first.").format(
|
||||
frappe.bold(existing_default_shift_factor)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestAssetShiftFactor(FrappeTestCase):
|
||||
pass
|
||||
@@ -15,6 +15,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
|
||||
class TestAssetValueAdjustment(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_asset_data()
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
|
||||
)
|
||||
|
||||
def test_current_asset_value(self):
|
||||
pr = make_purchase_receipt(
|
||||
|
||||
@@ -1,318 +1,115 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 1,
|
||||
"autoname": "",
|
||||
"beta": 0,
|
||||
"creation": "2016-03-02 15:11:01.278862",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2016-03-02 15:11:01.278862",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"finance_book",
|
||||
"schedule_date",
|
||||
"depreciation_amount",
|
||||
"column_break_3",
|
||||
"accumulated_depreciation_amount",
|
||||
"journal_entry",
|
||||
"shift",
|
||||
"make_depreciation_entry",
|
||||
"finance_book_id",
|
||||
"depreciation_method"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Finance Book",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Finance Book",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Schedule Date",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Schedule Date",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "depreciation_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Depreciation Amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "depreciation_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Depreciation Amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "accumulated_depreciation_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Accumulated Depreciation Amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "accumulated_depreciation_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Accumulated Depreciation Amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
"fieldname": "journal_entry",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Journal Entry",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Journal Entry",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
"fieldname": "journal_entry",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Journal Entry",
|
||||
"no_copy": 1,
|
||||
"options": "Journal Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
|
||||
"fieldname": "make_depreciation_entry",
|
||||
"fieldtype": "Button",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Make Depreciation Entry",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
|
||||
"fieldname": "make_depreciation_entry",
|
||||
"fieldtype": "Button",
|
||||
"label": "Make Depreciation Entry"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "finance_book_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Finance Book Id",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "finance_book_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Finance Book Id",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "depreciation_method",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Depreciation Method",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "depreciation_method",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Depreciation Method",
|
||||
"no_copy": 1,
|
||||
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shift",
|
||||
"fieldtype": "Link",
|
||||
"label": "Shift",
|
||||
"options": "Asset Shift Factor"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-10 15:12:41.679436",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Depreciation Schedule",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 04:43:04.218580",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Depreciation Schedule",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,30 +1,21 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Bulk Transaction Log', {
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
|
||||
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
|
||||
query(frm, 1);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
frappe.ui.form.on("Bulk Transaction Log", {
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__('Succeeded Entries'), function() {
|
||||
frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"});
|
||||
}, __("View"));
|
||||
frm.add_custom_button(__('Failed Entries'), function() {
|
||||
frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Failed"});
|
||||
}, __("View"));
|
||||
if (frm.doc.failed) {
|
||||
frm.add_custom_button(__('Retry Failed Transactions'), function() {
|
||||
frappe.call({
|
||||
method: "erpnext.utilities.bulk_transaction.retry",
|
||||
args: {date: frm.doc.date}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function query(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
|
||||
args: {
|
||||
log_date: frm.doc.log_date
|
||||
}
|
||||
}).then((r) => {
|
||||
if (r.message === "No Failed Records") {
|
||||
frappe.show_alert(__(r.message), 5);
|
||||
} else {
|
||||
frappe.show_alert(__("Retrying Failed Transactions"), 5);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,31 +1,64 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-30 13:41:16.343827",
|
||||
"allow_copy": 1,
|
||||
"creation": "2023-11-09 20:14:45.139593",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"log_date",
|
||||
"logger_data"
|
||||
"date",
|
||||
"column_break_bsan",
|
||||
"log_entries",
|
||||
"section_break_mdmv",
|
||||
"succeeded",
|
||||
"column_break_qryp",
|
||||
"failed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "log_date",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Log Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "logger_data",
|
||||
"fieldtype": "Table",
|
||||
"label": "Logger Data",
|
||||
"options": "Bulk Transaction Log Detail"
|
||||
"fieldname": "log_entries",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Log Entries",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bsan",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_mdmv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "succeeded",
|
||||
"fieldtype": "Int",
|
||||
"label": "Succeeded",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qryp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "failed",
|
||||
"fieldtype": "Int",
|
||||
"label": "Failed",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"in_create": 1,
|
||||
"is_virtual": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 17:23:02.935325",
|
||||
"modified": "2023-11-11 04:52:49.347376",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log",
|
||||
@@ -47,5 +80,5 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
"title_field": "date"
|
||||
}
|
||||
@@ -1,67 +1,112 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.utilities.bulk_transaction import task, update_logger
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint
|
||||
from pypika import Order
|
||||
|
||||
|
||||
class BulkTransactionLog(Document):
|
||||
pass
|
||||
def db_insert(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def load_from_db(self):
|
||||
log_detail = qb.DocType("Bulk Transaction Log Detail")
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry_failing_transaction(log_date=None):
|
||||
if not log_date:
|
||||
log_date = str(date.today())
|
||||
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
|
||||
data = (
|
||||
frappe.qb.from_(btp)
|
||||
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
|
||||
.distinct()
|
||||
.where(btp.retried != 1)
|
||||
.where(btp.transaction_status == "Failed")
|
||||
.where(btp.date == log_date)
|
||||
).run(as_dict=True)
|
||||
has_records = frappe.db.sql(
|
||||
f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
|
||||
)[0][0]
|
||||
if not has_records:
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
if data:
|
||||
if len(data) > 10:
|
||||
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
|
||||
else:
|
||||
job(data, log_date)
|
||||
else:
|
||||
return "No Failed Records"
|
||||
succeeded_logs = (
|
||||
qb.from_(log_detail)
|
||||
.select(Count(log_detail.date).as_("count"))
|
||||
.where((log_detail.date == self.name) & (log_detail.transaction_status == "Success"))
|
||||
.run()
|
||||
)[0][0] or 0
|
||||
failed_logs = (
|
||||
qb.from_(log_detail)
|
||||
.select(Count(log_detail.date).as_("count"))
|
||||
.where((log_detail.date == self.name) & (log_detail.transaction_status == "Failed"))
|
||||
.run()
|
||||
)[0][0] or 0
|
||||
total_logs = succeeded_logs + failed_logs
|
||||
transaction_log = frappe._dict(
|
||||
{
|
||||
"date": self.name,
|
||||
"count": total_logs,
|
||||
"succeeded": succeeded_logs,
|
||||
"failed": failed_logs,
|
||||
}
|
||||
)
|
||||
super(Document, self).__init__(serialize_transaction_log(transaction_log))
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
filter_date = parse_list_filters(args)
|
||||
limit = cint(args.get("page_length")) or 20
|
||||
log_detail = qb.DocType("Bulk Transaction Log Detail")
|
||||
|
||||
def job(data, log_date):
|
||||
for d in data:
|
||||
failed = []
|
||||
try:
|
||||
frappe.db.savepoint("before_creation_of_record")
|
||||
task(d.transaction_name, d.from_doctype, d.to_doctype)
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point="before_creation_of_record")
|
||||
failed.append(e)
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
e,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Failed",
|
||||
log_date=log_date,
|
||||
restarted=1,
|
||||
dates_query = (
|
||||
qb.from_(log_detail)
|
||||
.select(log_detail.date)
|
||||
.distinct()
|
||||
.orderby(log_detail.date, order=Order.desc)
|
||||
.limit(limit)
|
||||
)
|
||||
if filter_date:
|
||||
dates_query = dates_query.where(log_detail.date == filter_date)
|
||||
dates = dates_query.run()
|
||||
|
||||
transaction_logs = []
|
||||
if dates:
|
||||
transaction_logs_query = (
|
||||
qb.from_(log_detail)
|
||||
.select(log_detail.date.as_("date"), Count(log_detail.date).as_("count"))
|
||||
.where(log_detail.date.isin(dates))
|
||||
.orderby(log_detail.date, order=Order.desc)
|
||||
.groupby(log_detail.date)
|
||||
.limit(limit)
|
||||
)
|
||||
transaction_logs = transaction_logs_query.run(as_dict=True)
|
||||
|
||||
if not failed:
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
None,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Success",
|
||||
log_date=log_date,
|
||||
restarted=1,
|
||||
)
|
||||
return [serialize_transaction_log(x) for x in transaction_logs]
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
pass
|
||||
|
||||
def db_update(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
pass
|
||||
|
||||
|
||||
def serialize_transaction_log(data):
|
||||
return frappe._dict(
|
||||
name=data.date,
|
||||
date=data.date,
|
||||
log_entries=data.count,
|
||||
succeeded=data.succeeded,
|
||||
failed=data.failed,
|
||||
)
|
||||
|
||||
|
||||
def parse_list_filters(args):
|
||||
# parse date filter
|
||||
filter_date = None
|
||||
for fil in args.get("filters"):
|
||||
if isinstance(fil, list):
|
||||
for elem in fil:
|
||||
if elem == "date":
|
||||
filter_date = fil[3]
|
||||
return filter_date
|
||||
|
||||
@@ -1,79 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.utilities.bulk_transaction import transaction_processing
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBulkTransactionLog(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_company()
|
||||
create_customer()
|
||||
create_item()
|
||||
|
||||
def test_entry_in_log(self):
|
||||
so_name = create_so()
|
||||
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
|
||||
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
|
||||
for d in doc.get("logger_data"):
|
||||
if d.transaction_name == so_name:
|
||||
self.assertEqual(d.transaction_name, so_name)
|
||||
self.assertEqual(d.transaction_status, "Success")
|
||||
self.assertEqual(d.from_doctype, "Sales Order")
|
||||
self.assertEqual(d.to_doctype, "Sales Invoice")
|
||||
self.assertEqual(d.retried, 0)
|
||||
|
||||
|
||||
def create_company():
|
||||
if not frappe.db.exists("Company", "_Test Company"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "_Test Company",
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_customer():
|
||||
if not frappe.db.exists("Customer", "Bulk Customer"):
|
||||
frappe.get_doc({"doctype": "Customer", "customer_name": "Bulk Customer"}).insert()
|
||||
|
||||
|
||||
def create_item():
|
||||
if not frappe.db.exists("Item", "MK"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item",
|
||||
"item_code": "MK",
|
||||
"item_name": "Milk",
|
||||
"description": "Milk",
|
||||
"item_group": "Products",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_so(intent=None):
|
||||
so = frappe.new_doc("Sales Order")
|
||||
so.customer = "Bulk Customer"
|
||||
so.company = "_Test Company"
|
||||
so.transaction_date = date.today()
|
||||
|
||||
so.set_warehouse = "Finished Goods - _TC"
|
||||
so.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "MK",
|
||||
"delivery_date": date.today(),
|
||||
"qty": 10,
|
||||
"rate": 80,
|
||||
},
|
||||
)
|
||||
so.insert()
|
||||
so.submit()
|
||||
return so.name
|
||||
class TestBulkTransactionLog(FrappeTestCase):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Bulk Transaction Log Detail", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -6,12 +6,12 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_doctype",
|
||||
"transaction_name",
|
||||
"date",
|
||||
"time",
|
||||
"transaction_status",
|
||||
"error_description",
|
||||
"from_doctype",
|
||||
"to_doctype",
|
||||
"retried"
|
||||
],
|
||||
@@ -20,8 +20,11 @@
|
||||
"fieldname": "transaction_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Name",
|
||||
"options": "from_doctype"
|
||||
"options": "from_doctype",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_status",
|
||||
@@ -39,9 +42,11 @@
|
||||
{
|
||||
"fieldname": "from_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "From Doctype",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_doctype",
|
||||
@@ -54,8 +59,10 @@
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Date ",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "time",
|
||||
@@ -66,19 +73,33 @@
|
||||
{
|
||||
"fieldname": "retried",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Retried",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 19:57:31.650359",
|
||||
"modified": "2023-11-10 11:44:10.758342",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBulkTransactionLogDetail(FrappeTestCase):
|
||||
pass
|
||||
@@ -17,6 +17,7 @@
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"blanket_order_allowance",
|
||||
"project_update_frequency",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
@@ -172,6 +173,14 @@
|
||||
"fieldname": "blanket_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Blanket Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "Each Transaction",
|
||||
"description": "How often should Project be updated of Total Purchase Cost ?",
|
||||
"fieldname": "project_update_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Update frequency of Project",
|
||||
"options": "Each Transaction\nManual"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -179,7 +188,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-25 14:03:32.520418",
|
||||
"modified": "2023-11-24 10:55:51.287327",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -86,6 +86,10 @@ class PurchaseOrder(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
mri_compare_fields = [["project", "="], ["item_code", "="]]
|
||||
if self.is_subcontracted:
|
||||
mri_compare_fields = [["project", "="]]
|
||||
|
||||
super(PurchaseOrder, self).validate_with_previous_doc(
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
@@ -108,7 +112,7 @@ class PurchaseOrder(BuyingController):
|
||||
},
|
||||
"Material Request Item": {
|
||||
"ref_dn_field": "material_request_item",
|
||||
"compare_fields": [["project", "="], ["item_code", "="]],
|
||||
"compare_fields": mri_compare_fields,
|
||||
"is_child_table": True,
|
||||
},
|
||||
}
|
||||
@@ -282,23 +286,6 @@ class PurchaseOrder(BuyingController):
|
||||
check_list.append(d.material_request)
|
||||
check_on_hold_or_closed_status("Material Request", d.material_request)
|
||||
|
||||
def update_requested_qty(self):
|
||||
material_request_map = {}
|
||||
for d in self.get("items"):
|
||||
if d.material_request_item:
|
||||
material_request_map.setdefault(d.material_request, []).append(d.material_request_item)
|
||||
|
||||
for mr, mr_item_rows in material_request_map.items():
|
||||
if mr and mr_item_rows:
|
||||
mr_obj = frappe.get_doc("Material Request", mr)
|
||||
|
||||
if mr_obj.status in ["Stopped", "Cancelled"]:
|
||||
frappe.throw(
|
||||
_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError
|
||||
)
|
||||
|
||||
mr_obj.update_requested_qty(mr_item_rows)
|
||||
|
||||
def update_ordered_qty(self, po_item_rows=None):
|
||||
"""update requested qty (before ordered_qty is updated)"""
|
||||
item_wh_list = []
|
||||
@@ -340,7 +327,9 @@ class PurchaseOrder(BuyingController):
|
||||
self.update_status_updater()
|
||||
|
||||
self.update_prevdoc_status()
|
||||
self.update_requested_qty()
|
||||
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
||||
self.update_requested_qty()
|
||||
|
||||
self.update_ordered_qty()
|
||||
self.validate_budget()
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
@@ -372,7 +361,9 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
# Must be called after updating ordered qty in Material Request
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
self.update_requested_qty()
|
||||
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
||||
self.update_requested_qty()
|
||||
|
||||
self.update_ordered_qty()
|
||||
|
||||
self.update_blanket_order()
|
||||
@@ -450,6 +441,20 @@ class PurchaseOrder(BuyingController):
|
||||
else:
|
||||
self.db_set("per_received", 0, update_modified=False)
|
||||
|
||||
def update_ordered_qty_in_so_for_removed_items(self, removed_items):
|
||||
"""
|
||||
Updates ordered_qty in linked SO when item rows are removed using Update Items
|
||||
"""
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = frappe.get_cached_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||
)
|
||||
|
||||
|
||||
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||
"""get last purchase rate for an item"""
|
||||
@@ -536,8 +541,6 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
doc.set_onload("ignore_price_list", True)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@@ -617,7 +620,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
postprocess,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
doc.set_onload("ignore_price_list", True)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -679,7 +681,10 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Subcontracting Order Service Item",
|
||||
"field_map": {},
|
||||
"field_map": {
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": [],
|
||||
},
|
||||
},
|
||||
@@ -705,8 +710,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
count = frappe.db.count(
|
||||
"Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]}
|
||||
return (
|
||||
True
|
||||
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
|
||||
else False
|
||||
)
|
||||
|
||||
return True if count else False
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as make_pi_from_po,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
@@ -27,6 +27,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_purchase_order_qty(self):
|
||||
po = create_purchase_order(qty=1, do_not_save=True)
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": -1,
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.NonNegativeError, po.save)
|
||||
|
||||
po.items[1].qty = 0
|
||||
self.assertRaises(InvalidQtyError, po.save)
|
||||
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -214,6 +215,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_width": "60px",
|
||||
@@ -917,7 +919,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-06 11:00:53.596417",
|
||||
"modified": "2023-11-24 19:07:34.921094",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
@@ -292,13 +294,25 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Document Print",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Billing Address",
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Billing Address Details",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-09 12:20:26.850623",
|
||||
"modified": "2023-11-06 12:45:28.898706",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -261,13 +262,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-24 17:26:46.276934",
|
||||
"modified": "2023-11-14 18:34:48.327224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -143,16 +143,17 @@ class Supplier(TransactionBase):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
supplier = filters.get("supplier")
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
`tabContact`.name from `tabContact`,
|
||||
`tabDynamic Link`
|
||||
WHERE
|
||||
`tabContact`.name = `tabDynamic Link`.parent
|
||||
and `tabDynamic Link`.link_name = %(supplier)s
|
||||
and `tabDynamic Link`.link_doctype = 'Supplier'
|
||||
and `tabContact`.name like %(txt)s
|
||||
""",
|
||||
{"supplier": supplier, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
contact = frappe.qb.DocType("Contact")
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(contact)
|
||||
.join(dynamic_link)
|
||||
.on(contact.name == dynamic_link.parent)
|
||||
.select(contact.name, contact.email_id)
|
||||
.where(
|
||||
(dynamic_link.link_name == supplier)
|
||||
& (dynamic_link.link_doctype == "Supplier")
|
||||
& (contact.name.like("%{0}%".format(txt)))
|
||||
)
|
||||
).run(as_dict=False)
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
"valid_till",
|
||||
"quotation_number",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -79,6 +83,7 @@
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"address_and_contact_tab",
|
||||
"supplier_address_section",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
"column_break_72",
|
||||
@@ -86,6 +91,14 @@
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"shipping_address_section",
|
||||
"shipping_address",
|
||||
"column_break_zjaq",
|
||||
"shipping_address_display",
|
||||
"company_billing_address_section",
|
||||
"billing_address",
|
||||
"column_break_gcth",
|
||||
"billing_address_display",
|
||||
"terms_tab",
|
||||
"tc_name",
|
||||
"terms",
|
||||
@@ -837,6 +850,76 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Shipping Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zjaq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Shipping Address Details",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_billing_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Billing Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Billing Address",
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gcth",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Billing Address Details",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
@@ -844,7 +927,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-14 16:43:41.714832",
|
||||
"modified": "2023-11-20 11:15:30.083077",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
|
||||
@@ -168,7 +168,6 @@ def make_purchase_order(source_name, target_doc=None):
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
doclist.set_onload("ignore_price_list", True)
|
||||
return doclist
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
"column_break_15",
|
||||
"manufacturer_part_no",
|
||||
"ad_sec_break",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_44",
|
||||
"page_break"
|
||||
@@ -134,6 +136,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
@@ -554,19 +557,31 @@
|
||||
"fieldname": "expected_delivery_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Delivery Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-19 12:36:26.913211",
|
||||
"modified": "2023-11-17 12:25:26.235367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -70,6 +70,10 @@ class AccountMissingError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQtyError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
force_item_fields = (
|
||||
"item_group",
|
||||
"brand",
|
||||
@@ -238,7 +242,7 @@ class AccountsController(TransactionBase):
|
||||
references_map.setdefault(x.parent, []).append(x.name)
|
||||
|
||||
for doc, rows in references_map.items():
|
||||
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
|
||||
unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc)
|
||||
for row in rows:
|
||||
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
|
||||
|
||||
@@ -247,9 +251,9 @@ class AccountsController(TransactionBase):
|
||||
unreconcile_doc.save(ignore_permissions=True)
|
||||
|
||||
# delete docs upon parent doc deletion
|
||||
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
|
||||
unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name})
|
||||
for x in unreconcile_docs:
|
||||
_doc = frappe.get_doc("Unreconcile Payments", x.name)
|
||||
_doc = frappe.get_doc("Unreconcile Payment", x.name)
|
||||
if _doc.docstatus == 1:
|
||||
_doc.cancel()
|
||||
_doc.delete()
|
||||
@@ -895,10 +899,16 @@ class AccountsController(TransactionBase):
|
||||
return gl_dict
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
if self.doctype != "Purchase Receipt":
|
||||
for item in self.items:
|
||||
if not item.qty:
|
||||
frappe.throw(_("Item quantity can not be zero"))
|
||||
if self.doctype == "Purchase Receipt":
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if not flt(item.qty):
|
||||
frappe.throw(
|
||||
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
|
||||
title=_("Invalid Quantity"),
|
||||
exc=InvalidQtyError,
|
||||
)
|
||||
|
||||
def validate_account_currency(self, account, account_currency=None):
|
||||
valid_currency = [self.company_currency]
|
||||
@@ -2864,6 +2874,9 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
if parent.doctype == "Purchase Order":
|
||||
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||
|
||||
# need to update ordered qty in Material Request first
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
parent.update_prevdoc_status()
|
||||
@@ -3023,16 +3036,19 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
if flt(child_item.billed_amt, rate_precision) > flt(
|
||||
flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision
|
||||
):
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
|
||||
child_item.idx, child_item.item_code
|
||||
)
|
||||
)
|
||||
else:
|
||||
child_item.rate = flt(d.get("rate"), rate_precision)
|
||||
child_item.rate = row_rate
|
||||
|
||||
if d.get("conversion_factor"):
|
||||
if child_item.stock_uom == child_item.uom:
|
||||
@@ -3116,7 +3132,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
if parent_doctype == "Purchase Order":
|
||||
update_last_purchase_rate(parent, is_submit=1)
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
if any_qty_changed or items_added_or_removed or any_conversion_factor_changed:
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
parent.update_requested_qty()
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
|
||||
@@ -87,7 +87,8 @@ class BuyingController(SubcontractingController):
|
||||
"posting_date": self.get("posting_date"),
|
||||
"posting_time": self.get("posting_time"),
|
||||
"qty": row.qty,
|
||||
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
|
||||
"serial_no": row.serial_no,
|
||||
"batch_no": row.batch_no,
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
@@ -338,7 +339,7 @@ class BuyingController(SubcontractingController):
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
"posting_date": self.get("posting_date") or self.get("transation_date"),
|
||||
"posting_date": self.get("posting_date") or self.get("transaction_date"),
|
||||
"posting_time": posting_time,
|
||||
"qty": -1 * flt(d.get("stock_qty")),
|
||||
"serial_no": d.get("serial_no"),
|
||||
@@ -745,7 +746,7 @@ class BuyingController(SubcontractingController):
|
||||
"calculate_depreciation": 1,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"asset_quantity": row.qty if is_grouped_asset else 0,
|
||||
"asset_quantity": row.qty if is_grouped_asset else 1,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
"cost_center": row.cost_center,
|
||||
|
||||
@@ -560,6 +560,8 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
|
||||
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select tabAccount.name from `tabAccount`
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
|
||||
@@ -356,6 +356,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
|
||||
doc.consolidated_invoice = ""
|
||||
doc.set("payments", [])
|
||||
doc.update_billed_amount_in_delivery_note = True
|
||||
for data in source.payments:
|
||||
paid_amount = 0.00
|
||||
base_paid_amount = 0.00
|
||||
@@ -529,8 +530,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
doclist.set_onload("ignore_price_list", True)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
|
||||
@@ -344,11 +344,12 @@ class SellingController(StockController):
|
||||
return il
|
||||
|
||||
def has_product_bundle(self, item_code):
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabProduct Bundle`
|
||||
where new_item_code=%s and docstatus != 2""",
|
||||
item_code,
|
||||
)
|
||||
product_bundle = frappe.qb.DocType("Product Bundle")
|
||||
return (
|
||||
frappe.qb.from_(product_bundle)
|
||||
.select(product_bundle.name)
|
||||
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
|
||||
).run()
|
||||
|
||||
def get_already_delivered_qty(self, current_docname, so, so_detail):
|
||||
delivered_via_dn = frappe.db.sql(
|
||||
|
||||
@@ -789,6 +789,23 @@ class SubcontractingController(StockController):
|
||||
|
||||
return self._sub_contracted_items
|
||||
|
||||
def update_requested_qty(self):
|
||||
material_request_map = {}
|
||||
for d in self.get("items"):
|
||||
if d.material_request_item:
|
||||
material_request_map.setdefault(d.material_request, []).append(d.material_request_item)
|
||||
|
||||
for mr, mr_item_rows in material_request_map.items():
|
||||
if mr and mr_item_rows:
|
||||
mr_obj = frappe.get_doc("Material Request", mr)
|
||||
|
||||
if mr_obj.status in ["Stopped", "Cancelled"]:
|
||||
frappe.throw(
|
||||
_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError
|
||||
)
|
||||
|
||||
mr_obj.update_requested_qty(mr_item_rows)
|
||||
|
||||
|
||||
def get_item_details(items):
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
@@ -51,6 +51,7 @@ class calculate_taxes_and_totals(object):
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
|
||||
self.doc.grand_total -= self.doc.discount_amount
|
||||
self.doc.base_grand_total -= self.doc.base_discount_amount
|
||||
self.doc.rounding_adjustment = self.doc.base_rounding_adjustment = 0.0
|
||||
self.set_rounded_total()
|
||||
|
||||
self.calculate_shipping_charges()
|
||||
|
||||
@@ -938,6 +938,7 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA5": {},
|
||||
"Subcontracted Item SA6": {},
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
@@ -957,6 +958,7 @@ def make_raw_materials():
|
||||
},
|
||||
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
|
||||
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
|
||||
"Subcontracted SRM Item 8": {},
|
||||
}
|
||||
|
||||
for item, properties in raw_materials.items():
|
||||
@@ -980,6 +982,7 @@ def make_service_items():
|
||||
"Subcontracted Service Item 5": {},
|
||||
"Subcontracted Service Item 6": {},
|
||||
"Subcontracted Service Item 7": {},
|
||||
"Subcontracted Service Item 8": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
@@ -1003,6 +1006,7 @@ def make_bom_for_subcontracted_items():
|
||||
"Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
|
||||
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
|
||||
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
|
||||
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
|
||||
}
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
|
||||
@@ -29,8 +29,16 @@
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-21 12:43:59.106807",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Competitor Detail",
|
||||
"link_fieldname": "competitor",
|
||||
"parent_doctype": "Quotation",
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@@ -64,5 +72,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -37,6 +37,15 @@ class Lead(SellingController, CRMNote):
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if self.source == "Existing Customer" and self.customer:
|
||||
contact = frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{"link_doctype": "Customer", "parenttype": "Contact", "link_name": self.customer},
|
||||
"parent",
|
||||
)
|
||||
if contact:
|
||||
self.contact_doc = frappe.get_doc("Contact", contact)
|
||||
return
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user