mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 11:29:48 +00:00
Compare commits
302 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a22d3b9895 | ||
|
|
ad960c1470 | ||
|
|
0c7219159a | ||
|
|
431fa225e3 | ||
|
|
f27e35c8f4 | ||
|
|
5fbffcbd7b | ||
|
|
bb949da334 | ||
|
|
8764a321c7 | ||
|
|
49e3865265 | ||
|
|
33a1da8194 | ||
|
|
52309fe0b6 | ||
|
|
0fdd6817a6 | ||
|
|
17535095e2 | ||
|
|
4d74597f94 | ||
|
|
f9420db3ca | ||
|
|
8996685f44 | ||
|
|
7046a01921 | ||
|
|
0b8cf3a369 | ||
|
|
fe5de30256 | ||
|
|
20bb15167d | ||
|
|
1ccf30d97b | ||
|
|
524a8d77f7 | ||
|
|
667e659e3f | ||
|
|
77e92b38eb | ||
|
|
ff3425ead1 | ||
|
|
d098fd3fc3 | ||
|
|
a66d475b56 | ||
|
|
6a52f79cce | ||
|
|
49ffeccafa | ||
|
|
6bc210d9f4 | ||
|
|
f4b7fa8980 | ||
|
|
c331a4fa84 | ||
|
|
d42173beb5 | ||
|
|
611c1f1ec2 | ||
|
|
a05fb916ff | ||
|
|
b2d35fae10 | ||
|
|
8d13ef050e | ||
|
|
8ac40f07e3 | ||
|
|
46894a5b86 | ||
|
|
821cfe2c39 | ||
|
|
b8b76a5b58 | ||
|
|
01d2794968 | ||
|
|
02cfb589a2 | ||
|
|
caf5faceda | ||
|
|
07653c54f3 | ||
|
|
affa67e74d | ||
|
|
2d63fc98d0 | ||
|
|
8bb4415f65 | ||
|
|
90b2ec9aba | ||
|
|
24ae74ebb3 | ||
|
|
7d1d0c8e0c | ||
|
|
1f39bb9c7a | ||
|
|
9e60f67d25 | ||
|
|
6a91f9155f | ||
|
|
b3d545f91a | ||
|
|
77f612354f | ||
|
|
7b0e19499b | ||
|
|
34504a23ec | ||
|
|
b603adce5e | ||
|
|
9efc1de40e | ||
|
|
ff622ef552 | ||
|
|
49778432ea | ||
|
|
0e8a04f2cc | ||
|
|
2a40845f65 | ||
|
|
a85a103bad | ||
|
|
b7509e326e | ||
|
|
76ea858e72 | ||
|
|
97487eb6e1 | ||
|
|
97612ebbf7 | ||
|
|
02a0bf35ea | ||
|
|
0d17cf115a | ||
|
|
cca507f0bd | ||
|
|
e2b6615e72 | ||
|
|
fd2a1a0ba0 | ||
|
|
514835b5dd | ||
|
|
bcd3b36821 | ||
|
|
43a40b1e66 | ||
|
|
cf980a1304 | ||
|
|
0f687f1db7 | ||
|
|
a0063b31c2 | ||
|
|
47e2699315 | ||
|
|
899acc3601 | ||
|
|
56d06861d0 | ||
|
|
45bf65f37c | ||
|
|
443a0b81f9 | ||
|
|
d348d587e2 | ||
|
|
1da4712919 | ||
|
|
3dcb7c1380 | ||
|
|
ce7dc5ecb3 | ||
|
|
d5f30202b4 | ||
|
|
cdd3763a8b | ||
|
|
78e0799a06 | ||
|
|
27b7de4854 | ||
|
|
1b5384f8d4 | ||
|
|
498abf7c83 | ||
|
|
cc0a478559 | ||
|
|
07947eb3f4 | ||
|
|
1a089da51a | ||
|
|
67c9872146 | ||
|
|
d55fe7d33f | ||
|
|
177ca6f431 | ||
|
|
8915c804c1 | ||
|
|
d506589d10 | ||
|
|
7ef15ec082 | ||
|
|
c9d43f4c88 | ||
|
|
1093c227f1 | ||
|
|
d0c60343cd | ||
|
|
97d5d18041 | ||
|
|
197b13eb87 | ||
|
|
0986434871 | ||
|
|
7fa3a789b1 | ||
|
|
647f039598 | ||
|
|
00baa6a07f | ||
|
|
67ba639ae1 | ||
|
|
0179358f07 | ||
|
|
4cb27b97b4 | ||
|
|
422ada2726 | ||
|
|
d2ede713e4 | ||
|
|
e48022f2fe | ||
|
|
82a3fd13f2 | ||
|
|
561fe5a171 | ||
|
|
f8f2c39ed5 | ||
|
|
becd64fccb | ||
|
|
510e3cebc9 | ||
|
|
bd20ff581b | ||
|
|
b4faee24da | ||
|
|
d042330c30 | ||
|
|
d241862f14 | ||
|
|
8e34e20b27 | ||
|
|
caea52c280 | ||
|
|
4473b74e1b | ||
|
|
0a7832d8d3 | ||
|
|
3a7e335d4b | ||
|
|
8a9c457d37 | ||
|
|
13be763105 | ||
|
|
0688c0ab6b | ||
|
|
7e00e55d18 | ||
|
|
ecca59c8a8 | ||
|
|
2d12f2cebd | ||
|
|
c86174d0f2 | ||
|
|
744f3457de | ||
|
|
4a261b5cef | ||
|
|
bad744194f | ||
|
|
b571c7af87 | ||
|
|
4b4d5dfa63 | ||
|
|
4c8b4419b6 | ||
|
|
d83cc57e58 | ||
|
|
00898be8e4 | ||
|
|
225d4ccb63 | ||
|
|
e09232234e | ||
|
|
a460bf9433 | ||
|
|
30151da57c | ||
|
|
3c8b637a8b | ||
|
|
6fa338bfb7 | ||
|
|
9664790272 | ||
|
|
0a4090a771 | ||
|
|
ebc8bede7f | ||
|
|
19b64496b1 | ||
|
|
f9974f9eb0 | ||
|
|
0eccb01b7b | ||
|
|
ce04f6d7c9 | ||
|
|
424d585962 | ||
|
|
1a7f195d8f | ||
|
|
5b0222615d | ||
|
|
9986c26a0c | ||
|
|
e0c1e3474e | ||
|
|
17dad01ab7 | ||
|
|
9515625135 | ||
|
|
bad1b2a164 | ||
|
|
e2aedc85b4 | ||
|
|
68fe3b4165 | ||
|
|
aed90f73d7 | ||
|
|
7a2b07a2a3 | ||
|
|
86ff1545ce | ||
|
|
e0b8c2788b | ||
|
|
ecc756bd52 | ||
|
|
0891399916 | ||
|
|
1436fda041 | ||
|
|
639a71ffd0 | ||
|
|
1f0f31a773 | ||
|
|
3bb4699754 | ||
|
|
3620ddab34 | ||
|
|
8233e22d8b | ||
|
|
1f293edc9e | ||
|
|
13cd12e1e2 | ||
|
|
dfbb2279b6 | ||
|
|
e1d288321f | ||
|
|
c502b562db | ||
|
|
15e3663633 | ||
|
|
c1a1346e2a | ||
|
|
1e02d06424 | ||
|
|
2812eae589 | ||
|
|
a94e3cd433 | ||
|
|
91f6393104 | ||
|
|
7d724d7647 | ||
|
|
e4b811be23 | ||
|
|
ccd7992e99 | ||
|
|
86ff3e0c10 | ||
|
|
96fe3f4a2c | ||
|
|
d7583e3993 | ||
|
|
5533592b2b | ||
|
|
ba19e06e64 | ||
|
|
1988c23539 | ||
|
|
70cac3186b | ||
|
|
bed6dabecb | ||
|
|
c4103d26be | ||
|
|
2061c7ca46 | ||
|
|
84bd0c8c50 | ||
|
|
f884cf8a5e | ||
|
|
54782d41be | ||
|
|
d7caa8d51b | ||
|
|
9e8150554f | ||
|
|
df0dac5610 | ||
|
|
cbce4e72d9 | ||
|
|
2c2a204a06 | ||
|
|
da5b57e4af | ||
|
|
de5b253215 | ||
|
|
a8ef90b40c | ||
|
|
7e847f27dd | ||
|
|
eb809847f1 | ||
|
|
9baaaca924 | ||
|
|
c911a70a84 | ||
|
|
e2728db5c2 | ||
|
|
f6f1052a80 | ||
|
|
8529eb4573 | ||
|
|
a73fedbaca | ||
|
|
147eb047aa | ||
|
|
9ae04dfed3 | ||
|
|
78ab44ce1a | ||
|
|
0079bd282e | ||
|
|
2db54f0bb1 | ||
|
|
627a50e7bc | ||
|
|
7eafe7f853 | ||
|
|
6e563438bd | ||
|
|
0eb5e001e1 | ||
|
|
0dfb9ddb85 | ||
|
|
d7e6b0e1b7 | ||
|
|
fef7cfb888 | ||
|
|
aa442da2f6 | ||
|
|
6aebe98020 | ||
|
|
841df3b1ce | ||
|
|
cc5ada194e | ||
|
|
edccb746f0 | ||
|
|
506fd97a2c | ||
|
|
6c9485fb13 | ||
|
|
4f806f9a72 | ||
|
|
4723fbfd57 | ||
|
|
ab49ee2e05 | ||
|
|
d1295d1c79 | ||
|
|
8a45258778 | ||
|
|
d10a9d3225 | ||
|
|
3643c60c67 | ||
|
|
92e252e37a | ||
|
|
b9efeeb71a | ||
|
|
d695fb5723 | ||
|
|
2eb70026ae | ||
|
|
46a8500361 | ||
|
|
88ba24f1cf | ||
|
|
18cbc956bb | ||
|
|
47e4fd3c76 | ||
|
|
071adcf291 | ||
|
|
87bb403159 | ||
|
|
43f59da0a9 | ||
|
|
ee647f2381 | ||
|
|
2c865bcd49 | ||
|
|
fc783f5acf | ||
|
|
24126b03dd | ||
|
|
5fa4fd8825 | ||
|
|
23fb4f348f | ||
|
|
227c912ece | ||
|
|
5bfbdb805c | ||
|
|
ec40131d4d | ||
|
|
859672c419 | ||
|
|
e8c3617628 | ||
|
|
09b21b8cb4 | ||
|
|
356da69179 | ||
|
|
bd0f11ef4f | ||
|
|
9f3dfb3d18 | ||
|
|
24b5b3c8e0 | ||
|
|
a7de8c1143 | ||
|
|
2fcab327aa | ||
|
|
f3cbbef346 | ||
|
|
339beff023 | ||
|
|
85d398efcc | ||
|
|
e09f101336 | ||
|
|
f9b52b292e | ||
|
|
8a94d7bea8 | ||
|
|
f871f08f47 | ||
|
|
313ea3983f | ||
|
|
c3e2ff2fa5 | ||
|
|
33e835c4d3 | ||
|
|
c9fa4af4fe | ||
|
|
36898f6797 | ||
|
|
b9a0f4fed8 | ||
|
|
1019d98c5a | ||
|
|
cc9b22ce9f | ||
|
|
f5135cd4a4 | ||
|
|
137ef78d96 | ||
|
|
5452f8ac4a | ||
|
|
f03e301250 | ||
|
|
c23868a14d | ||
|
|
eb5505187e |
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
@@ -10,6 +10,7 @@ WEBSITE_REPOS = [
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"docs.frappe.io",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
|
||||
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
|
||||
name: Patch Test
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.76.0"
|
||||
__version__ = "14.80.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -94,8 +94,8 @@ frappe.ui.form.on("Account", {
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
account: frm.doc.name,
|
||||
from_date: frappe.sys_defaults.year_start_date,
|
||||
to_date: frappe.sys_defaults.year_end_date,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company: frm.doc.company,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -279,8 +279,8 @@ frappe.treeview_settings["Account"] = {
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: frappe.sys_defaults.year_start_date,
|
||||
to_date: frappe.sys_defaults.year_end_date,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -284,54 +285,56 @@ def create_payment_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
|
||||
payment_entry.update(payment_entry_dict)
|
||||
bank_currency = bank_transaction.currency
|
||||
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
payment_entry.paid_from = company_account
|
||||
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
|
||||
|
||||
payment_entry.validate()
|
||||
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
|
||||
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = company
|
||||
pe.reference_no = reference_number
|
||||
pe.reference_date = reference_date
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pe.posting_date = posting_date
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank_account
|
||||
pe.paid_to = party_account if payment_type == "Pay" else bank_account
|
||||
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
|
||||
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
|
||||
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
|
||||
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
|
||||
pe.mode_of_payment = mode_of_payment
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
return payment_entry
|
||||
return pe
|
||||
|
||||
payment_entry.insert()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
"payment_name": pe.name,
|
||||
"amount": amt_in_bank_acc_currency,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -455,8 +458,12 @@ def get_linked_payments(
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
voucher_docs = [(voucher[1], voucher[2]) for voucher in vouchers]
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher[1], voucher[2])
|
||||
rows = voucher_allocated_amounts.get((voucher[1], voucher[2])) or []
|
||||
amount = None
|
||||
for row in rows:
|
||||
if row["gl_account"] == gl_account:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.utils import flt
|
||||
|
||||
@@ -92,10 +93,16 @@ class BankTransaction(StatusUpdater):
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self, payment_entry
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
@@ -156,13 +163,17 @@ class BankTransaction(StatusUpdater):
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
result = None
|
||||
try:
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
|
||||
|
||||
if result:
|
||||
party_type, party = result
|
||||
@@ -177,7 +188,7 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
@@ -186,7 +197,6 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -242,44 +252,52 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
return result
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
btp.payment_entry
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
btp.payment_document = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
dict(docs=docs),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
return result
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
@@ -460,13 +460,20 @@ def get_actual_expense(args):
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
|
||||
@@ -52,6 +52,21 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def before_submit(self):
|
||||
self.remove_accounts_without_gain_loss()
|
||||
|
||||
def remove_accounts_without_gain_loss(self):
|
||||
self.accounts = [account for account in self.accounts if account.gain_loss]
|
||||
|
||||
if not self.accounts:
|
||||
frappe.throw(_("At least one account with exchange gain or loss is required"))
|
||||
|
||||
frappe.msgprint(
|
||||
_("Removing rows without exchange gain or loss"),
|
||||
alert=True,
|
||||
indicator="yellow",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@@ -226,23 +241,23 @@ class ExchangeRateRevaluation(Document):
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
@@ -266,23 +281,22 @@ class ExchangeRateRevaluation(Document):
|
||||
current_exchange_rate * d.balance_in_account_currency
|
||||
)
|
||||
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
@@ -161,6 +165,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
company: doc.company,
|
||||
status: ["!=", "Paid"],
|
||||
outstanding_amount: [">", 0], // for compatibility with old data
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -320,6 +328,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
payment_type: function(frm) {
|
||||
set_default_party_type(frm);
|
||||
if(frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(["party", "party_balance", "paid_from", "paid_to",
|
||||
"references", "total_allocated_amount"], function(i, field) {
|
||||
@@ -1079,6 +1088,24 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (r.message) {
|
||||
if (!frm.doc.mode_of_payment) {
|
||||
frm.set_value(field, r.message.account);
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Mode of Payment Account",
|
||||
filters: {
|
||||
parent: frm.doc.mode_of_payment,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
fieldname: "default_account",
|
||||
parent: "Mode of Payment",
|
||||
},
|
||||
callback: function (res) {
|
||||
if (!res.message.default_account) {
|
||||
frm.set_value(field, r.message.account);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
frm.set_value('bank', r.message.bank);
|
||||
frm.set_value('bank_account_no', r.message.bank_account_no);
|
||||
@@ -1511,3 +1538,16 @@ frappe.ui.form.on('Payment Entry Deduction', {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
|
||||
function set_default_party_type(frm) {
|
||||
if (frm.doc.party) return;
|
||||
|
||||
let party_type;
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
party_type = "Customer";
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
party_type = "Supplier";
|
||||
}
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
@@ -1523,7 +1523,7 @@ class PaymentEntry(AccountsController):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
_("Cannot {0} from {1} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
@@ -2605,6 +2605,7 @@ def get_open_payment_requests_for_references(references=None):
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
@@ -144,12 +144,14 @@ class PaymentReconciliation(Document):
|
||||
if self.get("cost_center"):
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
account_type = erpnext.get_party_account_type(self.party_type)
|
||||
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||
|
||||
conditions.append(dr_or_cr.gt(0))
|
||||
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
@@ -164,7 +166,7 @@ class PaymentReconciliation(Document):
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
dr_or_cr.as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
@@ -299,6 +301,10 @@ class PaymentReconciliation(Document):
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
non_reconciled_invoices = sorted(
|
||||
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
|
||||
)
|
||||
|
||||
self.add_invoice_entries(non_reconciled_invoices)
|
||||
|
||||
def add_invoice_entries(self, non_reconciled_invoices):
|
||||
|
||||
@@ -615,6 +615,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.accounts[1].credit_in_account_currency = 0
|
||||
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
@@ -937,6 +973,100 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = -8000
|
||||
je1.accounts[0].credit = -8000
|
||||
je1.accounts[0].debit_in_account_currency = 0
|
||||
je1.accounts[0].debit = 0
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = -16000
|
||||
je2.accounts[0].credit = -16000
|
||||
je2.accounts[0].debit_in_account_currency = 0
|
||||
je2.accounts[0].debit = 0
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
|
||||
@@ -838,21 +838,17 @@ def validate_payment(doc, method=None):
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
if not filters.reference_doctype or not filters.reference_name:
|
||||
return []
|
||||
|
||||
if txt:
|
||||
filters.name = ["like", f"%{txt}%"]
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
filters=filters,
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "payment_request",
|
||||
"internal_links": {
|
||||
"Payment Entry": ["references", "payment_request"],
|
||||
"Payment Order": ["references", "payment_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||
],
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "gray",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
Requested: "green",
|
||||
};
|
||||
|
||||
frappe.listview_settings["Payment Request"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Draft") {
|
||||
return [__("Draft"), "gray", "status,=,Draft"];
|
||||
}
|
||||
if (doc.status == "Requested") {
|
||||
return [__("Requested"), "green", "status,=,Requested"];
|
||||
} else if (doc.status == "Initiated") {
|
||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
||||
} else if (doc.status == "Partially Paid") {
|
||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
||||
} else if (doc.status == "Paid") {
|
||||
return [__("Paid"), "blue", "status,=,Paid"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||
|
||||
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1557,12 +1558,19 @@
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:00:34.268756",
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -328,6 +328,8 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get("child_docname"),
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -359,7 +361,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
if isinstance(pricing_rule, str):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
|
||||
pricing_rule.apply_rule_on_other_items = (
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
if args.coupon_code != coupon_code:
|
||||
continue
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -386,9 +401,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
if pricing_rule.price_or_product_discount == "Price":
|
||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||
|
||||
@@ -987,6 +987,45 @@ class TestPricingRule(FrappeTestCase):
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_pricing_rule_for_product_free_item_round_free_qty(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate": 0,
|
||||
"min_qty": 100,
|
||||
"max_qty": 0,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 1,
|
||||
"free_qty": 10,
|
||||
"round_free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 100,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With pricing rule
|
||||
so = make_sales_order(item_code="_Test Item", qty=100)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=150)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
@@ -642,7 +642,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
@@ -366,6 +366,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
hide_fields(this.frm.doc);
|
||||
if(cint(this.frm.doc.is_paid)) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
this.frm.set_value("payment_terms_template", "");
|
||||
this.frm.set_value("payment_schedule", []);
|
||||
if(!this.frm.doc.company) {
|
||||
this.frm.set_value("is_paid", 0)
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
|
||||
@@ -1443,7 +1443,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if pi:
|
||||
pi = pi[0][0]
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
frappe.throw(
|
||||
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
|
||||
get_link_to_form("Purchase Invoice", pi)
|
||||
)
|
||||
)
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
|
||||
@@ -1643,6 +1643,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -40,7 +41,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -111,13 +112,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 15:47:28.167371",
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -27,7 +27,7 @@ class RepostAccountingLedger(Document):
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
filters={"company": self.company, "docstatus": 1},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
limit=1,
|
||||
|
||||
@@ -667,20 +667,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
}
|
||||
|
||||
frm.set_query('company_address', function(doc) {
|
||||
if(!doc.company) {
|
||||
frappe.throw(__('Please set Company'));
|
||||
}
|
||||
|
||||
return {
|
||||
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||
filters: {
|
||||
link_doctype: 'Company',
|
||||
link_name: doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('pos_profile', function(doc) {
|
||||
if(!doc.company) {
|
||||
frappe.throw(_('Please set Company'));
|
||||
|
||||
@@ -160,8 +160,9 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_addr_col_break",
|
||||
"company_address_display",
|
||||
"company_addr_col_break",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -2171,6 +2172,13 @@
|
||||
"label": "Update Outstanding for Self",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2183,7 +2191,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -1556,8 +1556,12 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
def update_project(self):
|
||||
if self.project:
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
unique_projects.append(self.project)
|
||||
|
||||
for p in unique_projects:
|
||||
project = frappe.get_doc("Project", p)
|
||||
project.update_billed_amount()
|
||||
project.db_update()
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_item_tax_map
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
@@ -2817,13 +2818,26 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = "Net Total"
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
@@ -3760,6 +3774,102 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors INR",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="INR",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party INR").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "INR"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
def test_total_billed_amount(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "250px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +42,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,13 +113,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 15:47:46.911595",
|
||||
"modified": "2024-12-20 11:58:28.962370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
||||
@@ -124,6 +124,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
cost_center = get_cost_center(inv)
|
||||
tax_row.update({"cost_center": cost_center})
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
@@ -215,14 +218,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
"pan_no": pan_no,
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"valid_from": ("<=", posting_date),
|
||||
"valid_upto": (">=", posting_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
@@ -270,7 +273,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -507,7 +510,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
@@ -519,13 +522,15 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
has_cumulative_threshold_breached = (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
@@ -533,10 +538,8 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -161,6 +161,45 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_tax_on_excess_amount(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertTrue(len(pi1.taxes) > 0)
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
|
||||
@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render_address
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render_address
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
@@ -985,10 +991,4 @@ def add_party_account(party_type, party, company, account):
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
return frappe.call(_render_address, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -129,7 +129,6 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
@@ -145,6 +144,9 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -270,9 +272,6 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -529,9 +528,7 @@ class ReceivablePayableReport:
|
||||
self.append_payment_term(row, d, term)
|
||||
|
||||
def append_payment_term(self, row, d, term):
|
||||
if (
|
||||
self.filters.get("customer") or self.filters.get("supplier")
|
||||
) and d.currency == d.party_account_currency:
|
||||
if d.currency == d.party_account_currency:
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = d.base_payment_amount
|
||||
@@ -1004,22 +1001,29 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label="Party Type",
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
if self.account_type == "Receivable":
|
||||
label = _("Receivable Account")
|
||||
elif self.account_type == "Payable":
|
||||
label = _("Payable Account")
|
||||
else:
|
||||
label = _("Party Account")
|
||||
|
||||
self.add_column(
|
||||
label=self.account_type + " Account",
|
||||
label=label,
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -1028,10 +1032,10 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
label = _("Supplier Name")
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
label = _("Customer Name")
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
label=label,
|
||||
@@ -1057,7 +1061,7 @@ class ReceivablePayableReport:
|
||||
width=180,
|
||||
)
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
|
||||
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -101,6 +102,9 @@ def execute(filters=None):
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
return columns, data, message, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
gl_entries_by_account,
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
@@ -525,9 +526,15 @@ def get_accounting_entries(
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
query = query.where(gl_entry.account.isin(accounts))
|
||||
|
||||
entries = query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
return entries
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
|
||||
@@ -653,3 +660,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
|
||||
filtered_summary_list.append(period)
|
||||
|
||||
return filtered_summary_list
|
||||
|
||||
|
||||
def compute_growth_view_data(data, columns):
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
for column_idx in range(1, len(columns)):
|
||||
previous_period_key = columns[column_idx - 1].get("key")
|
||||
current_period_key = columns[column_idx].get("key")
|
||||
current_period_value = data_copy[row_idx].get(current_period_key)
|
||||
previous_period_value = data_copy[row_idx].get(previous_period_key)
|
||||
annual_growth = 0
|
||||
|
||||
if current_period_value is None:
|
||||
data[row_idx][current_period_key] = None
|
||||
continue
|
||||
|
||||
if previous_period_value == 0 and current_period_value > 0:
|
||||
annual_growth = 1
|
||||
|
||||
elif previous_period_value > 0:
|
||||
annual_growth = (current_period_value - previous_period_value) / previous_period_value
|
||||
|
||||
growth_percent = round(annual_growth * 100, 2)
|
||||
|
||||
data[row_idx][current_period_key] = growth_percent
|
||||
|
||||
|
||||
def compute_margin_view_data(data, columns, accumulated_values):
|
||||
if not columns:
|
||||
return
|
||||
|
||||
if not accumulated_values:
|
||||
columns.append({"key": "total"})
|
||||
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
base_row = None
|
||||
for row in data_copy:
|
||||
if row.get("account_name") == _("Income"):
|
||||
base_row = row
|
||||
break
|
||||
|
||||
if not base_row:
|
||||
return
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
# Taking the total income from each column (for all the financial years) as the base (100%)
|
||||
row = data_copy[row_idx]
|
||||
if not row:
|
||||
continue
|
||||
|
||||
for column in columns:
|
||||
curr_period = column.get("key")
|
||||
base_value = base_row[curr_period]
|
||||
curr_value = row[curr_period]
|
||||
|
||||
if curr_value is None or base_value <= 0:
|
||||
data[row_idx][curr_period] = None
|
||||
continue
|
||||
|
||||
margin_percent = round((curr_value / base_value) * 100, 2)
|
||||
|
||||
data[row_idx][curr_period] = margin_percent
|
||||
|
||||
@@ -402,10 +402,10 @@ class GrossProfitGenerator:
|
||||
self.load_invoice_items()
|
||||
self.get_delivery_notes()
|
||||
|
||||
self.load_product_bundle()
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
self.get_returned_invoice_items()
|
||||
self.process()
|
||||
@@ -421,6 +421,7 @@ class GrossProfitGenerator:
|
||||
|
||||
if grouped_by_invoice:
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.filters.get("group_by") == "Monthly":
|
||||
@@ -461,12 +462,11 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
|
||||
|
||||
if grouped_by_invoice:
|
||||
if row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
elif row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
buying_amount = 0
|
||||
if grouped_by_invoice and row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
row.base_amount = base_amount
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
# get buying rate
|
||||
if flt(row.qty):
|
||||
@@ -476,11 +476,19 @@ class GrossProfitGenerator:
|
||||
if self.is_not_invoice_row(row):
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
|
||||
if self.is_not_invoice_row(row):
|
||||
self.update_return_invoices(row)
|
||||
|
||||
if grouped_by_invoice and row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
row.gross_profit_percent = 0.0
|
||||
@@ -491,33 +499,29 @@ class GrossProfitGenerator:
|
||||
if self.grouped:
|
||||
self.get_average_rate_based_on_group_by()
|
||||
|
||||
def update_return_invoices(self, row):
|
||||
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
returned_item_row.qty = 0
|
||||
returned_item_row.base_amount = 0
|
||||
|
||||
else:
|
||||
row.qty = 0
|
||||
row.base_amount = 0
|
||||
returned_item_row.qty += row.qty
|
||||
returned_item_row.base_amount += row.base_amount
|
||||
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") == "Invoice":
|
||||
for row in self.grouped[key]:
|
||||
if row.indent == 1.0:
|
||||
if (
|
||||
row.parent in self.returned_invoices
|
||||
and row.item_code in self.returned_invoices[row.parent]
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(
|
||||
flt(row.qty) * flt(row.buying_rate), self.currency_precision
|
||||
)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
elif self.filters.get("group_by") == "Payment Term":
|
||||
if self.filters.get("group_by") == "Payment Term":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
invoice_portion = 0
|
||||
|
||||
@@ -537,7 +541,7 @@ class GrossProfitGenerator:
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
elif self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
@@ -613,6 +617,7 @@ class GrossProfitGenerator:
|
||||
if packed_item.get("parent_detail_docname") == row.item_row:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -645,7 +650,9 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
if (row.update_stock or row.dn_detail) and my_sle:
|
||||
parenttype, parent = row.parenttype, row.parent
|
||||
parenttype = row.parenttype
|
||||
parent = row.invoice or row.parent
|
||||
|
||||
if row.dn_detail:
|
||||
parenttype, parent = "Delivery Note", row.delivery_note
|
||||
|
||||
@@ -719,12 +726,13 @@ class GrossProfitGenerator:
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
.where(purchase_invoice.is_return == 0)
|
||||
.where(purchase_invoice_item.parenttype == "Purchase Invoice")
|
||||
)
|
||||
|
||||
if row.project:
|
||||
@@ -761,7 +769,10 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
|
||||
sales_person_cols = """, sales.sales_person,
|
||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
||||
sales.incentives
|
||||
"""
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
else:
|
||||
sales_person_cols = ""
|
||||
@@ -793,6 +804,7 @@ class GrossProfitGenerator:
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
@@ -853,6 +865,7 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
grouped = OrderedDict()
|
||||
product_bundles = self.product_bundles.get("Sales Invoice", {})
|
||||
|
||||
for row in self.si_list:
|
||||
# initialize list with a header row for each new parent
|
||||
@@ -863,8 +876,7 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
@@ -900,47 +912,40 @@ class GrossProfitGenerator:
|
||||
"item_row": None,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"),
|
||||
"base_net_amount": row.invoice_base_net_total,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
)
|
||||
|
||||
def get_bundle_item_row(self, product_bundle, item):
|
||||
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
|
||||
|
||||
def get_bundle_item_row(self, row, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": product_bundle.item_code,
|
||||
"indent": product_bundle.indent + 1,
|
||||
"parent_invoice": row.item_code,
|
||||
"parenttype": row.parenttype,
|
||||
"indent": row.indent + 1,
|
||||
"parent": None,
|
||||
"invoice_or_item": item.item_code,
|
||||
"posting_date": product_bundle.posting_date,
|
||||
"posting_time": product_bundle.posting_time,
|
||||
"project": product_bundle.project,
|
||||
"customer": product_bundle.customer,
|
||||
"customer_group": product_bundle.customer_group,
|
||||
"posting_date": row.posting_date,
|
||||
"posting_time": row.posting_time,
|
||||
"project": row.project,
|
||||
"customer": row.customer,
|
||||
"customer_group": row.customer_group,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item_name,
|
||||
"description": description,
|
||||
"warehouse": product_bundle.warehouse,
|
||||
"item_group": item_group,
|
||||
"brand": brand,
|
||||
"dn_detail": product_bundle.dn_detail,
|
||||
"delivery_note": product_bundle.delivery_note,
|
||||
"qty": (flt(product_bundle.qty) * flt(item.qty)),
|
||||
"item_row": None,
|
||||
"is_return": product_bundle.is_return,
|
||||
"cost_center": product_bundle.cost_center,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"warehouse": item.warehouse or row.warehouse,
|
||||
"update_stock": row.update_stock,
|
||||
"item_group": "",
|
||||
"brand": "",
|
||||
"dn_detail": row.dn_detail,
|
||||
"delivery_note": row.delivery_note,
|
||||
"qty": item.total_qty * -1,
|
||||
"item_row": row.item_row,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_item_details(self, item_code):
|
||||
return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"])
|
||||
|
||||
def get_stock_ledger_entries(self, item_code, warehouse):
|
||||
if item_code and warehouse:
|
||||
if (item_code, warehouse) not in self.sle:
|
||||
|
||||
@@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase):
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit": 0.0,
|
||||
"gross_profit_%": 0.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
compute_margin_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -68,6 +70,12 @@ def execute(filters=None):
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
if filters.get("selected_view") == "Margin":
|
||||
compute_margin_view_data(data, period_list, filters.accumulated_values)
|
||||
|
||||
return columns, data, None, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -1,840 +0,0 @@
|
||||
[
|
||||
{
|
||||
"account_manager": null,
|
||||
"accounts": [],
|
||||
"companies": [],
|
||||
"credit_limits": [],
|
||||
"customer_details": null,
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_name": "_Test Customer",
|
||||
"customer_pos_id": null,
|
||||
"customer_primary_address": null,
|
||||
"customer_primary_contact": null,
|
||||
"customer_type": "Company",
|
||||
"default_bank_account": null,
|
||||
"default_commission_rate": 0.0,
|
||||
"default_currency": null,
|
||||
"default_price_list": null,
|
||||
"default_sales_partner": null,
|
||||
"disabled": 0,
|
||||
"dn_required": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Customer",
|
||||
"email_id": null,
|
||||
"gender": null,
|
||||
"image": null,
|
||||
"industry": null,
|
||||
"is_frozen": 0,
|
||||
"is_internal_customer": 0,
|
||||
"language": "en",
|
||||
"lead_name": null,
|
||||
"loyalty_program": null,
|
||||
"loyalty_program_tier": null,
|
||||
"market_segment": null,
|
||||
"mobile_no": null,
|
||||
"modified": "2021-02-15 05:18:03.624724",
|
||||
"name": "_Test Customer",
|
||||
"naming_series": "CUST-.YYYY.-",
|
||||
"pan": null,
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"payment_terms": null,
|
||||
"primary_address": null,
|
||||
"represents_company": "",
|
||||
"sales_team": [],
|
||||
"salutation": null,
|
||||
"so_required": 0,
|
||||
"tax_category": null,
|
||||
"tax_id": null,
|
||||
"tax_withholding_category": null,
|
||||
"territory": "All Territories",
|
||||
"website": null
|
||||
},{
|
||||
"accounts": [],
|
||||
"allow_purchase_invoice_creation_without_purchase_order": 0,
|
||||
"allow_purchase_invoice_creation_without_purchase_receipt": 0,
|
||||
"companies": [],
|
||||
"country": "United Kingdom",
|
||||
"default_bank_account": null,
|
||||
"default_currency": null,
|
||||
"default_price_list": null,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Supplier",
|
||||
"hold_type": "",
|
||||
"image": null,
|
||||
"is_frozen": 0,
|
||||
"is_internal_supplier": 0,
|
||||
"is_transporter": 0,
|
||||
"language": "en",
|
||||
"modified": "2021-03-31 16:47:10.109316",
|
||||
"name": "_Test Supplier",
|
||||
"naming_series": "SUP-.YYYY.-",
|
||||
"on_hold": 0,
|
||||
"pan": null,
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"payment_terms": null,
|
||||
"prevent_pos": 0,
|
||||
"prevent_rfqs": 0,
|
||||
"release_date": null,
|
||||
"represents_company": null,
|
||||
"supplier_details": null,
|
||||
"supplier_group": "Raw Material",
|
||||
"supplier_name": "_Test Supplier",
|
||||
"supplier_type": "Company",
|
||||
"tax_category": null,
|
||||
"tax_id": null,
|
||||
"tax_withholding_category": null,
|
||||
"warn_pos": 0,
|
||||
"warn_rfqs": 0,
|
||||
"website": null
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "Debtors",
|
||||
"account_number": "",
|
||||
"account_type": "Receivable",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 58,
|
||||
"modified": "2021-03-26 04:44:19.955468",
|
||||
"name": "Debtors - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Application of Funds (Assets) - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Balance Sheet",
|
||||
"rgt": 59,
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "Sales",
|
||||
"account_number": "",
|
||||
"account_type": "Income Account",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 291,
|
||||
"modified": "2021-03-26 04:50:21.697703",
|
||||
"name": "Sales - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Income - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Profit and Loss",
|
||||
"rgt": 292,
|
||||
"root_type": "Income",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "VAT on Sales",
|
||||
"account_number": "",
|
||||
"account_type": "Tax",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 317,
|
||||
"modified": "2021-03-26 04:50:21.697703",
|
||||
"name": "VAT on Sales - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Source of Funds (Liabilities) - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Balance Sheet",
|
||||
"rgt": 318,
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "Cost of Goods Sold",
|
||||
"account_number": "",
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 171,
|
||||
"modified": "2021-03-26 04:44:19.994857",
|
||||
"name": "Cost of Goods Sold - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Expenses - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Profit and Loss",
|
||||
"rgt": 172,
|
||||
"root_type": "Expense",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "VAT on Purchases",
|
||||
"account_number": "",
|
||||
"account_type": "Tax",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 80,
|
||||
"modified": "2021-03-26 04:44:19.961983",
|
||||
"name": "VAT on Purchases - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Application of Funds (Assets) - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Balance Sheet",
|
||||
"rgt": 81,
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"account_currency": "GBP",
|
||||
"account_name": "Creditors",
|
||||
"account_number": "",
|
||||
"account_type": "Payable",
|
||||
"balance_must_be": "",
|
||||
"company": "_T",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Account",
|
||||
"freeze_account": "No",
|
||||
"include_in_gross": 0,
|
||||
"inter_company_account": 0,
|
||||
"is_group": 0,
|
||||
"lft": 302,
|
||||
"modified": "2021-03-26 04:50:21.697703",
|
||||
"name": "Creditors - _T",
|
||||
"old_parent": null,
|
||||
"parent": null,
|
||||
"parent_account": "Source of Funds (Liabilities) - _T",
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"report_type": "Balance Sheet",
|
||||
"rgt": 303,
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.0
|
||||
},{
|
||||
"additional_discount_percentage": 0.0,
|
||||
"address_display": null,
|
||||
"adjust_advance_taxes": 0,
|
||||
"advances": [],
|
||||
"against_expense_account": "Cost of Goods Sold - _T",
|
||||
"allocate_advances_automatically": 0,
|
||||
"amended_from": null,
|
||||
"apply_discount_on": "Grand Total",
|
||||
"apply_tds": 0,
|
||||
"auto_repeat": null,
|
||||
"base_discount_amount": 0.0,
|
||||
"base_grand_total": 511.68,
|
||||
"base_in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
|
||||
"base_net_total": 426.4,
|
||||
"base_paid_amount": 0.0,
|
||||
"base_rounded_total": 511.68,
|
||||
"base_rounding_adjustment": 0.0,
|
||||
"base_taxes_and_charges_added": 85.28,
|
||||
"base_taxes_and_charges_deducted": 0.0,
|
||||
"base_total": 426.4,
|
||||
"base_total_taxes_and_charges": 85.28,
|
||||
"base_write_off_amount": 0.0,
|
||||
"bill_date": null,
|
||||
"bill_no": null,
|
||||
"billing_address": null,
|
||||
"billing_address_display": null,
|
||||
"buying_price_list": "Standard Buying",
|
||||
"cash_bank_account": null,
|
||||
"clearance_date": null,
|
||||
"company": "_T",
|
||||
"contact_display": null,
|
||||
"contact_email": null,
|
||||
"contact_mobile": null,
|
||||
"contact_person": null,
|
||||
"conversion_rate": 1.0,
|
||||
"cost_center": null,
|
||||
"credit_to": "Creditors - _T",
|
||||
"currency": "GBP",
|
||||
"disable_rounded_total": 0,
|
||||
"discount_amount": 0.0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Purchase Invoice",
|
||||
"due_date": null,
|
||||
"from_date": null,
|
||||
"grand_total": 511.68,
|
||||
"group_same_items": 0,
|
||||
"hold_comment": null,
|
||||
"ignore_pricing_rule": 0,
|
||||
"in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
|
||||
"inter_company_invoice_reference": null,
|
||||
"is_internal_supplier": 0,
|
||||
"is_opening": "No",
|
||||
"is_paid": 0,
|
||||
"is_return": 0,
|
||||
"is_subcontracted": 0,
|
||||
"items": [
|
||||
{
|
||||
"allow_zero_valuation_rate": 0,
|
||||
"amount": 426.4,
|
||||
"asset_category": null,
|
||||
"asset_location": null,
|
||||
"base_amount": 426.4,
|
||||
"base_net_amount": 426.4,
|
||||
"base_net_rate": 5.33,
|
||||
"base_price_list_rate": 5.33,
|
||||
"base_rate": 5.33,
|
||||
"base_rate_with_margin": 0.0,
|
||||
"batch_no": null,
|
||||
"bom": null,
|
||||
"brand": null,
|
||||
"conversion_factor": 0.0,
|
||||
"cost_center": "Main - _T",
|
||||
"deferred_expense_account": null,
|
||||
"description": "<div class=\"ql-editor read-mode\"><p>Fluid to make widgets</p></div>",
|
||||
"discount_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"enable_deferred_expense": 0,
|
||||
"expense_account": "Cost of Goods Sold - _T",
|
||||
"from_warehouse": null,
|
||||
"image": null,
|
||||
"include_exploded_items": 0,
|
||||
"is_fixed_asset": 0,
|
||||
"is_free_item": 0,
|
||||
"item_code": null,
|
||||
"item_group": null,
|
||||
"item_name": "Widget Fluid 1Litre",
|
||||
"item_tax_amount": 0.0,
|
||||
"item_tax_rate": "{\"VAT on Purchases - _T\": 20.0}",
|
||||
"item_tax_template": null,
|
||||
"landed_cost_voucher_amount": 0.0,
|
||||
"manufacturer": null,
|
||||
"manufacturer_part_no": null,
|
||||
"margin_rate_or_amount": 0.0,
|
||||
"margin_type": "",
|
||||
"net_amount": 426.4,
|
||||
"net_rate": 5.33,
|
||||
"page_break": 0,
|
||||
"parent": null,
|
||||
"parentfield": "items",
|
||||
"parenttype": "Purchase Invoice",
|
||||
"po_detail": null,
|
||||
"pr_detail": null,
|
||||
"price_list_rate": 5.33,
|
||||
"pricing_rules": null,
|
||||
"project": null,
|
||||
"purchase_invoice_item": null,
|
||||
"purchase_order": null,
|
||||
"purchase_receipt": null,
|
||||
"qty": 80.0,
|
||||
"quality_inspection": null,
|
||||
"rate": 5.33,
|
||||
"rate_with_margin": 0.0,
|
||||
"received_qty": 0.0,
|
||||
"rejected_qty": 0.0,
|
||||
"rejected_serial_no": null,
|
||||
"rejected_warehouse": null,
|
||||
"rm_supp_cost": 0.0,
|
||||
"sales_invoice_item": null,
|
||||
"serial_no": null,
|
||||
"service_end_date": null,
|
||||
"service_start_date": null,
|
||||
"service_stop_date": null,
|
||||
"stock_qty": 0.0,
|
||||
"stock_uom": "Nos",
|
||||
"stock_uom_rate": 0.0,
|
||||
"total_weight": 0.0,
|
||||
"uom": "Nos",
|
||||
"valuation_rate": 0.0,
|
||||
"warehouse": null,
|
||||
"weight_per_unit": 0.0,
|
||||
"weight_uom": null
|
||||
}
|
||||
],
|
||||
"language": "en",
|
||||
"letter_head": null,
|
||||
"mode_of_payment": null,
|
||||
"modified": "2021-04-03 03:33:09.180453",
|
||||
"name": null,
|
||||
"naming_series": "ACC-PINV-.YYYY.-",
|
||||
"net_total": 426.4,
|
||||
"on_hold": 0,
|
||||
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Purchases</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Widget Fluid 1Litre</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 426.40\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 85.28\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
|
||||
"outstanding_amount": 511.68,
|
||||
"paid_amount": 0.0,
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"party_account_currency": "GBP",
|
||||
"payment_schedule": [],
|
||||
"payment_terms_template": null,
|
||||
"plc_conversion_rate": 1.0,
|
||||
"posting_date": null,
|
||||
"posting_time": "16:59:56.789522",
|
||||
"price_list_currency": "GBP",
|
||||
"pricing_rules": [],
|
||||
"project": null,
|
||||
"rejected_warehouse": null,
|
||||
"release_date": null,
|
||||
"remarks": "No Remarks",
|
||||
"represents_company": null,
|
||||
"return_against": null,
|
||||
"rounded_total": 511.68,
|
||||
"rounding_adjustment": 0.0,
|
||||
"scan_barcode": null,
|
||||
"select_print_heading": null,
|
||||
"set_from_warehouse": null,
|
||||
"set_posting_time": 0,
|
||||
"set_warehouse": null,
|
||||
"shipping_address": null,
|
||||
"shipping_address_display": "",
|
||||
"shipping_rule": null,
|
||||
"status": "Unpaid",
|
||||
"supplied_items": [],
|
||||
"supplier": "_Test Supplier",
|
||||
"supplier_address": null,
|
||||
"supplier_name": "_Test Supplier",
|
||||
"supplier_warehouse": "Stores - _T",
|
||||
"tax_category": null,
|
||||
"tax_id": null,
|
||||
"tax_withholding_category": null,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "VAT on Purchases - _T",
|
||||
"add_deduct_tax": "Add",
|
||||
"base_tax_amount": 85.28,
|
||||
"base_tax_amount_after_discount_amount": 85.28,
|
||||
"base_total": 511.68,
|
||||
"category": "Total",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "Main - _T",
|
||||
"description": "VAT on Purchases",
|
||||
"included_in_print_rate": 0,
|
||||
"item_wise_tax_detail": "{\"Widget Fluid 1Litre\":[20.0,85.28]}",
|
||||
"parent": null,
|
||||
"parentfield": "taxes",
|
||||
"parenttype": "Purchase Invoice",
|
||||
"rate": 0.0,
|
||||
"row_id": null,
|
||||
"tax_amount": 85.28,
|
||||
"tax_amount_after_discount_amount": 85.28,
|
||||
"total": 511.68
|
||||
}
|
||||
],
|
||||
"taxes_and_charges": null,
|
||||
"taxes_and_charges_added": 85.28,
|
||||
"taxes_and_charges_deducted": 0.0,
|
||||
"tc_name": null,
|
||||
"terms": null,
|
||||
"title": "_Purchase Invoice",
|
||||
"to_date": null,
|
||||
"total": 426.4,
|
||||
"total_advance": 0.0,
|
||||
"total_net_weight": 0.0,
|
||||
"total_qty": 80.0,
|
||||
"total_taxes_and_charges": 85.28,
|
||||
"unrealized_profit_loss_account": null,
|
||||
"update_stock": 0,
|
||||
"write_off_account": null,
|
||||
"write_off_amount": 0.0,
|
||||
"write_off_cost_center": null
|
||||
},{
|
||||
"account_for_change_amount": null,
|
||||
"additional_discount_percentage": 0.0,
|
||||
"address_display": null,
|
||||
"advances": [],
|
||||
"against_income_account": "Sales - _T",
|
||||
"allocate_advances_automatically": 0,
|
||||
"amended_from": null,
|
||||
"apply_discount_on": "Grand Total",
|
||||
"auto_repeat": null,
|
||||
"base_change_amount": 0.0,
|
||||
"base_discount_amount": 0.0,
|
||||
"base_grand_total": 868.25,
|
||||
"base_in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
|
||||
"base_net_total": 825.0,
|
||||
"base_paid_amount": 0.0,
|
||||
"base_rounded_total": 868.25,
|
||||
"base_rounding_adjustment": 0.0,
|
||||
"base_total": 825.0,
|
||||
"base_total_taxes_and_charges": 43.25,
|
||||
"base_write_off_amount": 0.0,
|
||||
"c_form_applicable": "No",
|
||||
"c_form_no": null,
|
||||
"campaign": null,
|
||||
"cash_bank_account": null,
|
||||
"change_amount": 0.0,
|
||||
"commission_rate": 0.0,
|
||||
"company": "_T",
|
||||
"company_address": null,
|
||||
"company_address_display": null,
|
||||
"company_tax_id": null,
|
||||
"contact_display": null,
|
||||
"contact_email": null,
|
||||
"contact_mobile": null,
|
||||
"contact_person": null,
|
||||
"conversion_rate": 1.0,
|
||||
"cost_center": null,
|
||||
"currency": "GBP",
|
||||
"customer": "_Test Customer",
|
||||
"customer_address": null,
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_name": "_Test Customer",
|
||||
"debit_to": "Debtors - _T",
|
||||
"discount_amount": 0.0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Sales Invoice",
|
||||
"due_date": null,
|
||||
"from_date": null,
|
||||
"grand_total": 868.25,
|
||||
"group_same_items": 0,
|
||||
"ignore_pricing_rule": 0,
|
||||
"in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
|
||||
"inter_company_invoice_reference": null,
|
||||
"is_consolidated": 0,
|
||||
"is_discounted": 0,
|
||||
"is_internal_customer": 0,
|
||||
"is_opening": "No",
|
||||
"is_pos": 0,
|
||||
"is_return": 0,
|
||||
"items": [
|
||||
{
|
||||
"actual_batch_qty": 0.0,
|
||||
"actual_qty": 0.0,
|
||||
"allow_zero_valuation_rate": 0,
|
||||
"amount": 200.0,
|
||||
"asset": null,
|
||||
"barcode": null,
|
||||
"base_amount": 200.0,
|
||||
"base_net_amount": 200.0,
|
||||
"base_net_rate": 50.0,
|
||||
"base_price_list_rate": 0.0,
|
||||
"base_rate": 50.0,
|
||||
"base_rate_with_margin": 0.0,
|
||||
"batch_no": null,
|
||||
"brand": null,
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "Main - _T",
|
||||
"customer_item_code": null,
|
||||
"deferred_revenue_account": null,
|
||||
"delivered_by_supplier": 0,
|
||||
"delivered_qty": 0.0,
|
||||
"delivery_note": null,
|
||||
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
|
||||
"discount_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"dn_detail": null,
|
||||
"enable_deferred_revenue": 0,
|
||||
"expense_account": null,
|
||||
"finance_book": null,
|
||||
"image": null,
|
||||
"income_account": "Sales - _T",
|
||||
"incoming_rate": 0.0,
|
||||
"is_fixed_asset": 0,
|
||||
"is_free_item": 0,
|
||||
"item_code": null,
|
||||
"item_group": null,
|
||||
"item_name": "Dunlop tyres",
|
||||
"item_tax_rate": "{\"VAT on Sales - _T\": 20.0}",
|
||||
"item_tax_template": null,
|
||||
"margin_rate_or_amount": 0.0,
|
||||
"margin_type": "",
|
||||
"net_amount": 200.0,
|
||||
"net_rate": 50.0,
|
||||
"page_break": 0,
|
||||
"parent": null,
|
||||
"parentfield": "items",
|
||||
"parenttype": "Sales Invoice",
|
||||
"price_list_rate": 0.0,
|
||||
"pricing_rules": null,
|
||||
"project": null,
|
||||
"qty": 4.0,
|
||||
"quality_inspection": null,
|
||||
"rate": 50.0,
|
||||
"rate_with_margin": 0.0,
|
||||
"sales_invoice_item": null,
|
||||
"sales_order": null,
|
||||
"serial_no": null,
|
||||
"service_end_date": null,
|
||||
"service_start_date": null,
|
||||
"service_stop_date": null,
|
||||
"so_detail": null,
|
||||
"stock_qty": 4.0,
|
||||
"stock_uom": "Nos",
|
||||
"stock_uom_rate": 50.0,
|
||||
"target_warehouse": null,
|
||||
"total_weight": 0.0,
|
||||
"uom": "Nos",
|
||||
"warehouse": null,
|
||||
"weight_per_unit": 0.0,
|
||||
"weight_uom": null
|
||||
},
|
||||
{
|
||||
"actual_batch_qty": 0.0,
|
||||
"actual_qty": 0.0,
|
||||
"allow_zero_valuation_rate": 0,
|
||||
"amount": 65.0,
|
||||
"asset": null,
|
||||
"barcode": null,
|
||||
"base_amount": 65.0,
|
||||
"base_net_amount": 65.0,
|
||||
"base_net_rate": 65.0,
|
||||
"base_price_list_rate": 0.0,
|
||||
"base_rate": 65.0,
|
||||
"base_rate_with_margin": 0.0,
|
||||
"batch_no": null,
|
||||
"brand": null,
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "Main - _T",
|
||||
"customer_item_code": null,
|
||||
"deferred_revenue_account": null,
|
||||
"delivered_by_supplier": 0,
|
||||
"delivered_qty": 0.0,
|
||||
"delivery_note": null,
|
||||
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
|
||||
"discount_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"dn_detail": null,
|
||||
"enable_deferred_revenue": 0,
|
||||
"expense_account": null,
|
||||
"finance_book": null,
|
||||
"image": null,
|
||||
"income_account": "Sales - _T",
|
||||
"incoming_rate": 0.0,
|
||||
"is_fixed_asset": 0,
|
||||
"is_free_item": 0,
|
||||
"item_code": "",
|
||||
"item_group": null,
|
||||
"item_name": "Continental tyres",
|
||||
"item_tax_rate": "{\"VAT on Sales - _T\": 5.0}",
|
||||
"item_tax_template": null,
|
||||
"margin_rate_or_amount": 0.0,
|
||||
"margin_type": "",
|
||||
"net_amount": 65.0,
|
||||
"net_rate": 65.0,
|
||||
"page_break": 0,
|
||||
"parent": null,
|
||||
"parentfield": "items",
|
||||
"parenttype": "Sales Invoice",
|
||||
"price_list_rate": 0.0,
|
||||
"pricing_rules": null,
|
||||
"project": null,
|
||||
"qty": 1.0,
|
||||
"quality_inspection": null,
|
||||
"rate": 65.0,
|
||||
"rate_with_margin": 0.0,
|
||||
"sales_invoice_item": null,
|
||||
"sales_order": null,
|
||||
"serial_no": null,
|
||||
"service_end_date": null,
|
||||
"service_start_date": null,
|
||||
"service_stop_date": null,
|
||||
"so_detail": null,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": null,
|
||||
"stock_uom_rate": 65.0,
|
||||
"target_warehouse": null,
|
||||
"total_weight": 0.0,
|
||||
"uom": "Nos",
|
||||
"warehouse": null,
|
||||
"weight_per_unit": 0.0,
|
||||
"weight_uom": null
|
||||
},
|
||||
{
|
||||
"actual_batch_qty": 0.0,
|
||||
"actual_qty": 0.0,
|
||||
"allow_zero_valuation_rate": 0,
|
||||
"amount": 560.0,
|
||||
"asset": null,
|
||||
"barcode": null,
|
||||
"base_amount": 560.0,
|
||||
"base_net_amount": 560.0,
|
||||
"base_net_rate": 70.0,
|
||||
"base_price_list_rate": 0.0,
|
||||
"base_rate": 70.0,
|
||||
"base_rate_with_margin": 0.0,
|
||||
"batch_no": null,
|
||||
"brand": null,
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "Main - _T",
|
||||
"customer_item_code": null,
|
||||
"deferred_revenue_account": null,
|
||||
"delivered_by_supplier": 0,
|
||||
"delivered_qty": 0.0,
|
||||
"delivery_note": null,
|
||||
"description": "<div class=\"ql-editor read-mode\"><p>New</p></div>",
|
||||
"discount_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"dn_detail": null,
|
||||
"enable_deferred_revenue": 0,
|
||||
"expense_account": null,
|
||||
"finance_book": null,
|
||||
"image": null,
|
||||
"income_account": "Sales - _T",
|
||||
"incoming_rate": 0.0,
|
||||
"is_fixed_asset": 0,
|
||||
"is_free_item": 0,
|
||||
"item_code": null,
|
||||
"item_group": null,
|
||||
"item_name": "Toyo tyres",
|
||||
"item_tax_rate": "{\"VAT on Sales - _T\": 0.0}",
|
||||
"item_tax_template": null,
|
||||
"margin_rate_or_amount": 0.0,
|
||||
"margin_type": "",
|
||||
"net_amount": 560.0,
|
||||
"net_rate": 70.0,
|
||||
"page_break": 0,
|
||||
"parent": null,
|
||||
"parentfield": "items",
|
||||
"parenttype": "Sales Invoice",
|
||||
"price_list_rate": 0.0,
|
||||
"pricing_rules": null,
|
||||
"project": null,
|
||||
"qty": 8.0,
|
||||
"quality_inspection": null,
|
||||
"rate": 70.0,
|
||||
"rate_with_margin": 0.0,
|
||||
"sales_invoice_item": null,
|
||||
"sales_order": null,
|
||||
"serial_no": null,
|
||||
"service_end_date": null,
|
||||
"service_start_date": null,
|
||||
"service_stop_date": null,
|
||||
"so_detail": null,
|
||||
"stock_qty": 8.0,
|
||||
"stock_uom": null,
|
||||
"stock_uom_rate": 70.0,
|
||||
"target_warehouse": null,
|
||||
"total_weight": 0.0,
|
||||
"uom": "Nos",
|
||||
"warehouse": null,
|
||||
"weight_per_unit": 0.0,
|
||||
"weight_uom": null
|
||||
}
|
||||
],
|
||||
"language": "en",
|
||||
"letter_head": null,
|
||||
"loyalty_amount": 0.0,
|
||||
"loyalty_points": 0,
|
||||
"loyalty_program": null,
|
||||
"loyalty_redemption_account": null,
|
||||
"loyalty_redemption_cost_center": null,
|
||||
"modified": "2021-02-16 05:18:59.755144",
|
||||
"name": null,
|
||||
"naming_series": "ACC-SINV-.YYYY.-",
|
||||
"net_total": 825.0,
|
||||
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Sales</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Dunlop tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 200.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 40.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Continental tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 65.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(5.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 3.25\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Toyo tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 560.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(0.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 0.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
|
||||
"outstanding_amount": 868.25,
|
||||
"packed_items": [],
|
||||
"paid_amount": 0.0,
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"party_account_currency": "GBP",
|
||||
"payment_schedule": [],
|
||||
"payment_terms_template": null,
|
||||
"payments": [],
|
||||
"plc_conversion_rate": 1.0,
|
||||
"po_date": null,
|
||||
"po_no": "",
|
||||
"pos_profile": null,
|
||||
"posting_date": null,
|
||||
"posting_time": "5:19:02.994077",
|
||||
"price_list_currency": "GBP",
|
||||
"pricing_rules": [],
|
||||
"project": null,
|
||||
"redeem_loyalty_points": 0,
|
||||
"remarks": "No Remarks",
|
||||
"represents_company": "",
|
||||
"return_against": null,
|
||||
"rounded_total": 868.25,
|
||||
"rounding_adjustment": 0.0,
|
||||
"sales_partner": null,
|
||||
"sales_team": [],
|
||||
"scan_barcode": null,
|
||||
"select_print_heading": null,
|
||||
"selling_price_list": "Standard Selling",
|
||||
"set_posting_time": 0,
|
||||
"set_target_warehouse": null,
|
||||
"set_warehouse": null,
|
||||
"shipping_address": null,
|
||||
"shipping_address_name": "",
|
||||
"shipping_rule": null,
|
||||
"source": null,
|
||||
"status": "Overdue",
|
||||
"tax_category": "",
|
||||
"tax_id": null,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "VAT on Sales - _T",
|
||||
"base_tax_amount": 43.25,
|
||||
"base_tax_amount_after_discount_amount": 43.25,
|
||||
"base_total": 868.25,
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "Main - _T",
|
||||
"description": "VAT on Sales",
|
||||
"included_in_print_rate": 0,
|
||||
"item_wise_tax_detail": "{\"Dunlop tyres\":[20.0,40.0],\"Continental tyres\":[5.0,3.25],\"Toyo tyres\":[0.0,0.0]}",
|
||||
"parent": null,
|
||||
"parentfield": "taxes",
|
||||
"parenttype": "Sales Invoice",
|
||||
"rate": 0.0,
|
||||
"row_id": null,
|
||||
"tax_amount": 43.25,
|
||||
"tax_amount_after_discount_amount": 43.25,
|
||||
"total": 868.25
|
||||
}
|
||||
],
|
||||
"taxes_and_charges": null,
|
||||
"tc_name": null,
|
||||
"terms": null,
|
||||
"territory": "All Territories",
|
||||
"timesheets": [],
|
||||
"title": "_Sales Invoice",
|
||||
"to_date": null,
|
||||
"total": 825.0,
|
||||
"total_advance": 0.0,
|
||||
"total_billing_amount": 0.0,
|
||||
"total_commission": 0.0,
|
||||
"total_net_weight": 0.0,
|
||||
"total_qty": 13.0,
|
||||
"total_taxes_and_charges": 43.25,
|
||||
"unrealized_profit_loss_account": null,
|
||||
"update_billed_amount_in_sales_order": 0,
|
||||
"update_stock": 0,
|
||||
"write_off_account": null,
|
||||
"write_off_amount": 0.0,
|
||||
"write_off_cost_center": null,
|
||||
"write_off_outstanding_amount_automatically": 0
|
||||
}
|
||||
]
|
||||
@@ -1,220 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
getdate,
|
||||
)
|
||||
|
||||
from .tax_detail import filter_match, save_custom_report
|
||||
|
||||
|
||||
class TestTaxDetail(unittest.TestCase):
|
||||
def load_testdocs(self):
|
||||
from erpnext.accounts.utils import FiscalYearError, get_fiscal_year
|
||||
|
||||
datapath, _ = os.path.splitext(os.path.realpath(__file__))
|
||||
with open(datapath + ".json") as fp:
|
||||
docs = json.load(fp)
|
||||
|
||||
now = getdate()
|
||||
self.from_date = get_first_day(now)
|
||||
self.to_date = get_last_day(now)
|
||||
|
||||
try:
|
||||
get_fiscal_year(now, company="_T")
|
||||
except FiscalYearError:
|
||||
docs = [
|
||||
{
|
||||
"companies": [
|
||||
{
|
||||
"company": "_T",
|
||||
"parent": "_Test Fiscal",
|
||||
"parentfield": "companies",
|
||||
"parenttype": "Fiscal Year",
|
||||
}
|
||||
],
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal",
|
||||
"year_end_date": get_year_ending(now),
|
||||
"year_start_date": get_year_start(now),
|
||||
},
|
||||
*docs,
|
||||
]
|
||||
|
||||
docs = [
|
||||
{
|
||||
"abbr": "_T",
|
||||
"company_name": "_T",
|
||||
"country": "United Kingdom",
|
||||
"default_currency": "GBP",
|
||||
"doctype": "Company",
|
||||
"name": "_T",
|
||||
},
|
||||
*docs,
|
||||
]
|
||||
|
||||
for doc in docs:
|
||||
try:
|
||||
db_doc = frappe.get_doc(doc)
|
||||
if "Invoice" in db_doc.doctype:
|
||||
db_doc.due_date = add_to_date(now, days=1)
|
||||
db_doc.insert()
|
||||
# Create GL Entries:
|
||||
db_doc.submit()
|
||||
else:
|
||||
db_doc.insert(ignore_if_duplicate=True)
|
||||
except frappe.exceptions.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
def load_defcols(self):
|
||||
self.company = frappe.get_doc("Company", "_T")
|
||||
custom_report = frappe.get_doc("Report", "Tax Detail")
|
||||
self.default_columns, _ = custom_report.run_query_report(
|
||||
filters={
|
||||
"from_date": "2021-03-01",
|
||||
"to_date": "2021-03-31",
|
||||
"company": self.company.name,
|
||||
"mode": "run",
|
||||
"report_name": "Tax Detail",
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
def rm_testdocs(self):
|
||||
"Remove the Company and all data"
|
||||
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
|
||||
|
||||
create_transaction_deletion_request(self.company.name)
|
||||
|
||||
def test_report(self):
|
||||
self.load_testdocs()
|
||||
self.load_defcols()
|
||||
report_name = save_custom_report(
|
||||
"Tax Detail",
|
||||
"_Test Tax Detail",
|
||||
json.dumps(
|
||||
{
|
||||
"columns": self.default_columns,
|
||||
"sections": {
|
||||
"Box1": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Sales"}}},
|
||||
"Box2": {"Filter0": {"type": "filter", "filters": {"4": "Acquisition"}}},
|
||||
"Box3": {"Box1": {"type": "section"}, "Box2": {"type": "section"}},
|
||||
"Box4": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Purchases"}}},
|
||||
"Box5": {"Box3": {"type": "section"}, "Box4": {"type": "section"}},
|
||||
"Box6": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales"}}},
|
||||
"Box7": {"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax"}}},
|
||||
"Box8": {
|
||||
"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales", "12": "EU"}}
|
||||
},
|
||||
"Box9": {
|
||||
"Filter0": {
|
||||
"type": "filter",
|
||||
"filters": {"2": "Expense", "3": "!=Tax", "12": "EU"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"show_detail": 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
data = frappe.desk.query_report.run(
|
||||
report_name,
|
||||
filters={
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
"company": self.company.name,
|
||||
"mode": "run",
|
||||
"report_name": report_name,
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
self.assertListEqual(data.get("columns"), self.default_columns)
|
||||
expected = (
|
||||
("Box1", 43.25),
|
||||
("Box2", 0.0),
|
||||
("Box3", 43.25),
|
||||
("Box4", -85.28),
|
||||
("Box5", -42.03),
|
||||
("Box6", 825.0),
|
||||
("Box7", -426.40),
|
||||
("Box8", 0.0),
|
||||
("Box9", 0.0),
|
||||
)
|
||||
exrow = iter(expected)
|
||||
for row in data.get("result"):
|
||||
if row.get("voucher_no") and not row.get("posting_date"):
|
||||
label, value = next(exrow)
|
||||
self.assertDictEqual(row, {"voucher_no": label, "amount": value})
|
||||
self.assertListEqual(
|
||||
data.get("report_summary"),
|
||||
[{"label": label, "datatype": "Currency", "value": value} for label, value in expected],
|
||||
)
|
||||
|
||||
self.rm_testdocs()
|
||||
|
||||
def test_filter_match(self):
|
||||
# None - treated as -inf number except range
|
||||
self.assertTrue(filter_match(None, "!="))
|
||||
self.assertTrue(filter_match(None, "<"))
|
||||
self.assertTrue(filter_match(None, "<jjj"))
|
||||
self.assertTrue(filter_match(None, " : "))
|
||||
self.assertTrue(filter_match(None, ":56"))
|
||||
self.assertTrue(filter_match(None, ":de"))
|
||||
self.assertFalse(filter_match(None, "3.4"))
|
||||
self.assertFalse(filter_match(None, "="))
|
||||
self.assertFalse(filter_match(None, "=3.4"))
|
||||
self.assertFalse(filter_match(None, ">3.4"))
|
||||
self.assertFalse(filter_match(None, " <"))
|
||||
self.assertFalse(filter_match(None, "ew"))
|
||||
self.assertFalse(filter_match(None, " "))
|
||||
self.assertFalse(filter_match(None, " f :"))
|
||||
|
||||
# Numbers
|
||||
self.assertTrue(filter_match(3.4, "3.4"))
|
||||
self.assertTrue(filter_match(3.4, ".4"))
|
||||
self.assertTrue(filter_match(3.4, "3"))
|
||||
self.assertTrue(filter_match(-3.4, "< -3"))
|
||||
self.assertTrue(filter_match(-3.4, "> -4"))
|
||||
self.assertTrue(filter_match(3.4, "= 3.4 "))
|
||||
self.assertTrue(filter_match(3.4, "!=4.5"))
|
||||
self.assertTrue(filter_match(3.4, " 3 : 4 "))
|
||||
self.assertTrue(filter_match(0.0, " : "))
|
||||
self.assertFalse(filter_match(3.4, "=4.5"))
|
||||
self.assertFalse(filter_match(3.4, " = 3.4 "))
|
||||
self.assertFalse(filter_match(3.4, "!=3.4"))
|
||||
self.assertFalse(filter_match(3.4, ">6"))
|
||||
self.assertFalse(filter_match(3.4, "<-4.5"))
|
||||
self.assertFalse(filter_match(3.4, "4.5"))
|
||||
self.assertFalse(filter_match(3.4, "5:9"))
|
||||
|
||||
# Strings
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "SINV"))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "sinv"))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "-2021"))
|
||||
self.assertTrue(filter_match(" ACC-SINV-2021-00001", " acc"))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "=2021"))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "!=zz"))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", "< zzz "))
|
||||
self.assertTrue(filter_match("ACC-SINV-2021-00001", " : sinv "))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", " sinv :"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", " acc"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", "= 2021 "))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", "!=sinv"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", " >"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", ">aa"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", " <"))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", "< "))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", " ="))
|
||||
self.assertFalse(filter_match("ACC-SINV-2021-00001", "="))
|
||||
|
||||
# Date - always match
|
||||
self.assertTrue(filter_match(datetime.date(2021, 3, 19), " kdsjkldfs "))
|
||||
@@ -72,8 +72,8 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
|
||||
total_amount = grand_total = base_total
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
@@ -405,7 +405,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
],
|
||||
"Journal Entry": ["total_amount"],
|
||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
@@ -427,7 +427,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
elif doctype == "Payment Entry":
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
value = [entry.total_debit] * 3
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
@@ -29,10 +29,6 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("Sales Register", {}),
|
||||
("Sales Register", {"item_group": "All Item Groups"}),
|
||||
("Purchase Register", {}),
|
||||
(
|
||||
"Tax Detail",
|
||||
{"mode": "run", "report_name": "Tax Detail"},
|
||||
),
|
||||
]
|
||||
|
||||
OPTIONAL_FILTERS = {}
|
||||
|
||||
@@ -579,6 +579,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
rev_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d["dr_or_cr"] == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if jv_detail.get(rev_dr_or_cr):
|
||||
d["dr_or_cr"] = rev_dr_or_cr
|
||||
d["allocated_amount"] = d["allocated_amount"] * -1
|
||||
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||
|
||||
@@ -318,7 +318,7 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
|
||||
frm.dashboard.render_graph({
|
||||
title: "Asset Value",
|
||||
title: __("Asset Value"),
|
||||
data: {
|
||||
labels: x_intervals,
|
||||
datasets: [
|
||||
|
||||
@@ -916,13 +916,12 @@ class Asset(AccountsController):
|
||||
].expected_value_after_useful_life
|
||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
||||
|
||||
if (
|
||||
flt(value_after_depreciation) <= expected_value_after_useful_life
|
||||
or self.is_fully_depreciated
|
||||
):
|
||||
if flt(value_after_depreciation) <= expected_value_after_useful_life:
|
||||
status = "Fully Depreciated"
|
||||
self.is_fully_depreciated = 1
|
||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
self.is_fully_depreciated = 0
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
return status
|
||||
|
||||
@@ -454,7 +454,7 @@ def restore_asset(asset_name):
|
||||
|
||||
|
||||
def depreciate_asset(asset, date):
|
||||
if not asset.calculate_depreciation:
|
||||
if not asset.calculate_depreciation or asset.is_fully_depreciated:
|
||||
return
|
||||
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
@@ -18,6 +18,7 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
update_received_amount(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
@@ -60,7 +61,6 @@ def get_data(filters):
|
||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||
po_item.base_amount.as_("amount"),
|
||||
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
|
||||
"pending_amount"
|
||||
@@ -92,6 +92,39 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def update_received_amount(data):
|
||||
pr_data = get_received_amount_data(data)
|
||||
|
||||
for row in data:
|
||||
row.received_qty_amount = flt(pr_data.get(row.name))
|
||||
|
||||
|
||||
def get_received_amount_data(data):
|
||||
pr = frappe.qb.DocType("Purchase Receipt")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pr)
|
||||
.inner_join(pr_item)
|
||||
.on(pr_item.parent == pr.name)
|
||||
.select(
|
||||
pr_item.purchase_order_item,
|
||||
Sum(pr_item.base_amount).as_("received_qty_amount"),
|
||||
)
|
||||
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
|
||||
.groupby(pr_item.purchase_order_item)
|
||||
)
|
||||
|
||||
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
|
||||
|
||||
data = query.run()
|
||||
|
||||
if not data:
|
||||
return frappe._dict()
|
||||
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def prepare_data(data, filters):
|
||||
completed, pending = 0, 0
|
||||
pending_field = "pending_amount"
|
||||
@@ -147,7 +180,7 @@ def prepare_data(data, filters):
|
||||
|
||||
|
||||
def prepare_chart_data(pending, completed):
|
||||
labels = ["Amount to Bill", "Billed Amount"]
|
||||
labels = [_("Amount to Bill"), _("Billed Amount")]
|
||||
|
||||
return {
|
||||
"data": {"labels": labels, "datasets": [{"values": [pending, completed]}]},
|
||||
|
||||
@@ -346,13 +346,14 @@ class AccountsController(TransactionBase):
|
||||
== 1
|
||||
)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
)
|
||||
frappe.db.sql(
|
||||
"delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s",
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
frappe.qb.from_(gle).delete().where(
|
||||
(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
|
||||
).run()
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
frappe.qb.from_(sle).delete().where(
|
||||
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
|
||||
).run()
|
||||
|
||||
def validate_return_against_account(self):
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
|
||||
@@ -415,9 +416,16 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
if (
|
||||
self.is_return
|
||||
or (self.doctype == "Purchase Invoice" and self.is_paid)
|
||||
or (self.doctype == "Sales Invoice" and self.is_pos)
|
||||
or self.get("is_opening") == "Yes"
|
||||
):
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
@@ -1020,11 +1028,12 @@ class AccountsController(TransactionBase):
|
||||
def clear_unallocated_advances(self, childtype, parentfield):
|
||||
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tab{}` where parentfield={} and parent = {}
|
||||
and allocated_amount = 0""".format(childtype, "%s", "%s"),
|
||||
(parentfield, self.name),
|
||||
)
|
||||
doctype = frappe.qb.DocType(childtype)
|
||||
frappe.qb.from_(doctype).delete().where(
|
||||
(doctype.parentfield == parentfield)
|
||||
& (doctype.parent == self.name)
|
||||
& (doctype.allocated_amount == 0)
|
||||
).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_shipping_rule(self):
|
||||
@@ -1075,6 +1084,7 @@ class AccountsController(TransactionBase):
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry
|
||||
"difference_posting_date": self.posting_date,
|
||||
}
|
||||
|
||||
self.append("advances", advance_row)
|
||||
@@ -1325,7 +1335,6 @@ class AccountsController(TransactionBase):
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
args.get("difference_posting_date") if args else self.posting_date,
|
||||
@@ -1438,6 +1447,7 @@ class AccountsController(TransactionBase):
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
),
|
||||
"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
|
||||
"difference_posting_date": d.get("difference_posting_date"),
|
||||
}
|
||||
)
|
||||
lst.append(args)
|
||||
@@ -1964,11 +1974,9 @@ class AccountsController(TransactionBase):
|
||||
for adv in self.advances:
|
||||
consider_for_total_advance = True
|
||||
if adv.reference_name == linked_doc_name:
|
||||
frappe.db.sql(
|
||||
f"""delete from `tab{self.doctype} Advance`
|
||||
where name = %s""",
|
||||
adv.name,
|
||||
)
|
||||
doctype = frappe.qb.DocType(self.doctype + " Advance")
|
||||
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
|
||||
|
||||
consider_for_total_advance = False
|
||||
|
||||
if consider_for_total_advance:
|
||||
@@ -2181,6 +2189,9 @@ class AccountsController(TransactionBase):
|
||||
return
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.due_date and d.discount_date:
|
||||
d.validate_from_to_dates("discount_date", "due_date")
|
||||
|
||||
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
|
||||
frappe.throw(
|
||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
||||
@@ -2196,7 +2207,7 @@ class AccountsController(TransactionBase):
|
||||
frappe.throw(_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates))
|
||||
|
||||
def validate_payment_schedule_amount(self):
|
||||
if self.doctype == "Sales Invoice" and self.is_pos:
|
||||
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
party_account_currency = self.get("party_account_currency")
|
||||
@@ -2313,6 +2324,12 @@ class AccountsController(TransactionBase):
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
|
||||
# Determine if multi-currency journal entry is needed
|
||||
multi_currency = (
|
||||
primary_account_currency != default_currency or secondary_account_currency != default_currency
|
||||
)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
@@ -2337,7 +2354,7 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
# Update dimensions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
@@ -2346,17 +2363,58 @@ class AccountsController(TransactionBase):
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
# Calculate exchange rates if necessary
|
||||
if multi_currency:
|
||||
# Exchange rates for primary and secondary accounts
|
||||
exc_rate_primary_to_default = (
|
||||
1
|
||||
if primary_account_currency == default_currency
|
||||
else get_exchange_rate(primary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_default = (
|
||||
1
|
||||
if secondary_account_currency == default_currency
|
||||
else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_primary = (
|
||||
1
|
||||
if secondary_account_currency == primary_account_currency
|
||||
else get_exchange_rate(
|
||||
secondary_account_currency, primary_account_currency, self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
# Convert outstanding amount from secondary to primary account currency, if needed
|
||||
|
||||
os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default
|
||||
os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
# Calculate credit and debit values for reconciliation and advance entries
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.credit = os_in_default_currency
|
||||
|
||||
advance_entry.debit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.debit = os_in_default_currency
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.credit = os_in_default_currency
|
||||
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit = os_in_default_currency
|
||||
|
||||
# Set exchange rates for entries
|
||||
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
|
||||
advance_entry.exchange_rate = exc_rate_primary_to_default
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||
jv.multi_currency = 1
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
jv.multi_currency = multi_currency
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
|
||||
@@ -71,16 +71,13 @@ def validate_returned_items(doc):
|
||||
|
||||
valid_items = frappe._dict()
|
||||
|
||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
|
||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor, name"
|
||||
if doc.doctype != "Purchase Invoice":
|
||||
select_fields += ",serial_no, batch_no"
|
||||
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
select_fields += ",rejected_qty, received_qty"
|
||||
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
select_fields += ",name"
|
||||
|
||||
for d in frappe.db.sql(
|
||||
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
|
||||
doc.return_against,
|
||||
@@ -108,11 +105,13 @@ def validate_returned_items(doc):
|
||||
for d in doc.get("items"):
|
||||
key = d.item_code
|
||||
raise_exception = False
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]:
|
||||
field = frappe.scrub(doc.doctype) + "_item"
|
||||
if d.get(field):
|
||||
key = (d.item_code, d.get(field))
|
||||
raise_exception = True
|
||||
elif doc.doctype == "Delivery Note":
|
||||
key = (d.item_code, d.get("dn_detail"))
|
||||
|
||||
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
|
||||
if key not in valid_items:
|
||||
@@ -124,7 +123,7 @@ def validate_returned_items(doc):
|
||||
)
|
||||
else:
|
||||
ref = valid_items.get(key, frappe._dict())
|
||||
validate_quantity(doc, d, ref, valid_items, already_returned_items)
|
||||
validate_quantity(doc, key, d, ref, valid_items, already_returned_items)
|
||||
|
||||
if (
|
||||
ref.rate
|
||||
@@ -174,12 +173,12 @@ def validate_returned_items(doc):
|
||||
frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
|
||||
|
||||
|
||||
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
fields = ["stock_qty"]
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
|
||||
fields.extend(["received_qty", "rejected_qty"])
|
||||
|
||||
already_returned_data = already_returned_items.get(args.item_code) or {}
|
||||
already_returned_data = already_returned_items.get(key) or {}
|
||||
|
||||
company_currency = erpnext.get_company_currency(doc.company)
|
||||
stock_qty_precision = get_field_precision(
|
||||
@@ -262,15 +261,20 @@ def get_already_returned_items(doc):
|
||||
column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
|
||||
sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
|
||||
|
||||
field = (
|
||||
frappe.scrub(doc.doctype) + "_item"
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"]
|
||||
else "dn_detail"
|
||||
)
|
||||
data = frappe.db.sql(
|
||||
f"""
|
||||
select {column}
|
||||
select {column}, {field}
|
||||
from
|
||||
`tab{doc.doctype} Item` child, `tab{doc.doctype}` par
|
||||
where
|
||||
child.parent = par.name and par.docstatus = 1
|
||||
and par.is_return = 1 and par.return_against = %s
|
||||
group by item_code
|
||||
group by item_code, {field}
|
||||
""",
|
||||
doc.return_against,
|
||||
as_dict=1,
|
||||
@@ -280,7 +284,7 @@ def get_already_returned_items(doc):
|
||||
|
||||
for d in data:
|
||||
items.setdefault(
|
||||
d.item_code,
|
||||
(d.item_code, d.get(field)),
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": d.get("qty"),
|
||||
|
||||
@@ -68,19 +68,13 @@ class SellingController(StockController):
|
||||
if customer:
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
|
||||
fetch_payment_terms_template = False
|
||||
if self.get("__islocal") or self.company != frappe.db.get_value(
|
||||
self.doctype, self.name, "company"
|
||||
):
|
||||
fetch_payment_terms_template = True
|
||||
|
||||
party_details = _get_party_details(
|
||||
customer,
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
doctype=self.doctype,
|
||||
company=self.company,
|
||||
posting_date=self.get("posting_date"),
|
||||
fetch_payment_terms_template=fetch_payment_terms_template,
|
||||
fetch_payment_terms_template=self.has_value_changed("company"),
|
||||
party_address=self.customer_address,
|
||||
shipping_address=self.shipping_address_name,
|
||||
company_address=self.get("company_address"),
|
||||
@@ -167,6 +161,9 @@ class SellingController(StockController):
|
||||
|
||||
total = 0.0
|
||||
sales_team = self.get("sales_team")
|
||||
|
||||
self.validate_sales_team(sales_team)
|
||||
|
||||
for sales_person in sales_team:
|
||||
self.round_floats_in(sales_person)
|
||||
|
||||
@@ -186,6 +183,20 @@ class SellingController(StockController):
|
||||
if sales_team and total != 100.0:
|
||||
throw(_("Total allocated percentage for sales team should be 100"))
|
||||
|
||||
def validate_sales_team(self, sales_team):
|
||||
sales_persons = [d.sales_person for d in sales_team]
|
||||
|
||||
if not sales_persons:
|
||||
return
|
||||
|
||||
sales_person_status = frappe.db.get_all(
|
||||
"Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
|
||||
)
|
||||
|
||||
for row in sales_person_status:
|
||||
if not row.enabled:
|
||||
frappe.throw(_("Sales Person <b>{0}</b> is disabled.").format(row.name))
|
||||
|
||||
def validate_max_discount(self):
|
||||
for d in self.get("items"):
|
||||
if d.item_code:
|
||||
@@ -348,12 +359,32 @@ class SellingController(StockController):
|
||||
return il
|
||||
|
||||
def has_product_bundle(self, 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()
|
||||
product_bundle_items = getattr(self, "_product_bundle_items", None)
|
||||
if product_bundle_items is None:
|
||||
self._product_bundle_items = product_bundle_items = {}
|
||||
|
||||
if item_code not in product_bundle_items:
|
||||
self._fetch_product_bundle_items(item_code)
|
||||
|
||||
return product_bundle_items[item_code]
|
||||
|
||||
def _fetch_product_bundle_items(self, item_code):
|
||||
product_bundle_items = self._product_bundle_items
|
||||
items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items}
|
||||
# fetch for requisite item_code even if it is not in items
|
||||
items_to_fetch.add(item_code)
|
||||
|
||||
items_with_product_bundle = {
|
||||
row.new_item_code
|
||||
for row in frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": ("in", items_to_fetch), "disabled": 0},
|
||||
fields="new_item_code",
|
||||
)
|
||||
}
|
||||
|
||||
for item_code in items_to_fetch:
|
||||
product_bundle_items[item_code] = item_code in items_with_product_bundle
|
||||
|
||||
def get_already_delivered_qty(self, current_docname, so, so_detail):
|
||||
delivered_via_dn = frappe.db.sql(
|
||||
|
||||
@@ -94,7 +94,10 @@ status_map = {
|
||||
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
[
|
||||
"Completed",
|
||||
"eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
|
||||
],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_inclusive_tax,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ class calculate_taxes_and_totals:
|
||||
self.doc = doc
|
||||
frappe.flags.round_off_applicable_accounts = []
|
||||
|
||||
if doc.get("round_off_applicable_accounts_for_tax_withholding"):
|
||||
frappe.flags.round_off_applicable_accounts.append(
|
||||
doc.round_off_applicable_accounts_for_tax_withholding
|
||||
)
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
@@ -67,6 +72,7 @@ class calculate_taxes_and_totals:
|
||||
self.validate_conversion_rate()
|
||||
self.calculate_item_values()
|
||||
self.validate_item_tax_template()
|
||||
self.update_item_tax_map()
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_net_total()
|
||||
@@ -130,6 +136,14 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
)
|
||||
|
||||
def update_item_tax_map(self):
|
||||
for item in self.doc.items:
|
||||
item.item_tax_rate = get_item_tax_map(
|
||||
company=self.doc.get("company"),
|
||||
item_tax_template=item.item_tax_template,
|
||||
as_json=True,
|
||||
)
|
||||
|
||||
def validate_conversion_rate(self):
|
||||
# validate conversion rate
|
||||
company_currency = erpnext.get_company_currency(self.doc.company)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified": "2024-12-10 08:26:38.496003",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@@ -53,20 +53,25 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Code List", {
|
||||
refresh: (frm) => {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(frm);
|
||||
});
|
||||
}
|
||||
},
|
||||
setup: (frm) => {
|
||||
frm.savetrash = () => {
|
||||
frm.validate_form_action("Delete");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to delete {0}?<p>This action will also delete all associated Common Code documents.</p>",
|
||||
[frm.docname.bold()]
|
||||
),
|
||||
function () {
|
||||
return frappe.call({
|
||||
method: "frappe.client.delete",
|
||||
args: {
|
||||
doctype: frm.doctype,
|
||||
name: frm.docname,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Deleting {0} and all associated Common Code documents...", [
|
||||
frm.docname,
|
||||
]),
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.utils.play_sound("delete");
|
||||
frappe.model.clear_doc(frm.doctype, frm.docname);
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
frm.set_query("default_common_code", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
code_list: doc.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2024-09-29 06:55:03.920375",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"canonical_uri",
|
||||
"url",
|
||||
"default_common_code",
|
||||
"column_break_nkls",
|
||||
"version",
|
||||
"publisher",
|
||||
"publisher_id",
|
||||
"section_break_npxp",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "version",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Version"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Canonical URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nkls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_npxp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher_id",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"options": "URL"
|
||||
},
|
||||
{
|
||||
"description": "This value shall be used when no matching Common Code for a record is found.",
|
||||
"fieldname": "default_common_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Common Code",
|
||||
"options": "Common Code"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Common Code",
|
||||
"link_fieldname": "code_list"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-16 17:01:40.260293",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Code List",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lxml.etree import Element
|
||||
|
||||
|
||||
class CodeList(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
canonical_uri: DF.Data | None
|
||||
default_common_code: DF.Link | None
|
||||
description: DF.SmallText | None
|
||||
publisher: DF.Data | None
|
||||
publisher_id: DF.Data | None
|
||||
title: DF.Data | None
|
||||
url: DF.Data | None
|
||||
version: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_trash(self):
|
||||
if not frappe.flags.in_bulk_delete:
|
||||
self.__delete_linked_docs()
|
||||
|
||||
def __delete_linked_docs(self):
|
||||
self.db_set("default_common_code", None)
|
||||
|
||||
linked_docs = frappe.get_all(
|
||||
"Common Code",
|
||||
filters={"code_list": self.name},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
for doc in linked_docs:
|
||||
frappe.delete_doc("Common Code", doc.name)
|
||||
|
||||
def get_codes_for(self, doctype: str, name: str) -> tuple[str]:
|
||||
"""Get the applicable codes for a doctype and name"""
|
||||
return get_codes_for(self.name, doctype, name)
|
||||
|
||||
def get_docnames_for(self, doctype: str, code: str) -> tuple[str]:
|
||||
"""Get the mapped docnames for a doctype and code"""
|
||||
return get_docnames_for(self.name, doctype, code)
|
||||
|
||||
def get_default_code(self) -> str | None:
|
||||
"""Get the default common code for this code list"""
|
||||
return (
|
||||
frappe.db.get_value("Common Code", self.default_common_code, "common_code")
|
||||
if self.default_common_code
|
||||
else None
|
||||
)
|
||||
|
||||
def from_genericode(self, root: "Element"):
|
||||
"""Extract Code List details from genericode XML"""
|
||||
self.title = root.find(".//Identification/ShortName").text
|
||||
self.version = root.find(".//Identification/Version").text
|
||||
self.canonical_uri = root.find(".//CanonicalUri").text
|
||||
# optionals
|
||||
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
|
||||
if not self.publisher:
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
|
||||
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
|
||||
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
||||
|
||||
|
||||
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||
"""Return the common code for a given record"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
codes = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(CommonCode.common_code)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (DynamicLink.link_name == name)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(CommonCode.common_code)
|
||||
).run()
|
||||
|
||||
return tuple(c[0] for c in codes) if codes else ()
|
||||
|
||||
|
||||
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||
"""Return the record name for a given common code"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
docnames = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(DynamicLink.link_name)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (CommonCode.common_code == code)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(DynamicLink.idx)
|
||||
).run()
|
||||
|
||||
return tuple(d[0] for d in docnames) if docnames else ()
|
||||
|
||||
|
||||
def get_default_code(code_list: str) -> str | None:
|
||||
"""Return the default common code for a given code list"""
|
||||
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
|
||||
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
@@ -0,0 +1,218 @@
|
||||
frappe.provide("erpnext.edi");
|
||||
|
||||
erpnext.edi.import_genericode = function (listview_or_form) {
|
||||
let doctype = "Code List";
|
||||
let docname = undefined;
|
||||
if (listview_or_form.doc !== undefined) {
|
||||
docname = listview_or_form.doc.name;
|
||||
}
|
||||
new frappe.ui.FileUploader({
|
||||
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
allow_toggle_private: false,
|
||||
allow_take_photo: false,
|
||||
on_success: function (_file_doc, r) {
|
||||
listview_or_form.refresh();
|
||||
show_column_selection_dialog(r.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function show_column_selection_dialog(context) {
|
||||
let title_description = __("If there is no title column, use the code column for the title.");
|
||||
let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]);
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "code_list_info",
|
||||
options: `<div class="text-muted">${__(
|
||||
"You are importing data for the code list:"
|
||||
)} ${frappe.utils.get_form_link(
|
||||
"Code List",
|
||||
context.code_list,
|
||||
true,
|
||||
context.code_list_title
|
||||
)}</div>`,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "import_column",
|
||||
label: __("Import"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldname: "title_column",
|
||||
label: __("as Title"),
|
||||
fieldtype: "Select",
|
||||
reqd: 1,
|
||||
options: context.columns,
|
||||
default: default_title,
|
||||
description: default_title ? null : title_description,
|
||||
},
|
||||
{
|
||||
fieldname: "code_column",
|
||||
label: __("as Code"),
|
||||
fieldtype: "Select",
|
||||
options: context.columns,
|
||||
reqd: 1,
|
||||
default: get_default(context.columns, ["code", "Code", "value"]),
|
||||
},
|
||||
{
|
||||
fieldname: "filters_column",
|
||||
label: __("Filter"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
];
|
||||
|
||||
if (context.columns.length > 2) {
|
||||
fields.splice(5, 0, {
|
||||
fieldname: "description_column",
|
||||
label: __("as Description"),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.columns),
|
||||
default: get_default(context.columns, [
|
||||
"description",
|
||||
"Description",
|
||||
"remark",
|
||||
__("description"),
|
||||
__("Description"),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
// Add filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
fields.push({
|
||||
fieldname: `filter_${column}`,
|
||||
label: __("by {}", [column]),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.filterable_columns[column]),
|
||||
});
|
||||
}
|
||||
|
||||
fields.push(
|
||||
{
|
||||
fieldname: "preview_section",
|
||||
label: __("Preview"),
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "preview_html",
|
||||
fieldtype: "HTML",
|
||||
}
|
||||
);
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Select Columns and Filters"),
|
||||
fields: fields,
|
||||
primary_action_label: __("Import"),
|
||||
size: "large", // This will make the modal wider
|
||||
primary_action(values) {
|
||||
let filters = {};
|
||||
for (let field in values) {
|
||||
if (field.startsWith("filter_") && values[field]) {
|
||||
filters[field.replace("filter_", "")] = values[field];
|
||||
}
|
||||
}
|
||||
frappe
|
||||
.xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", {
|
||||
code_list_name: context.code_list,
|
||||
file_name: context.file,
|
||||
code_column: values.code_column,
|
||||
title_column: values.title_column,
|
||||
description_column: values.description_column,
|
||||
filters: filters,
|
||||
})
|
||||
.then((count) => {
|
||||
frappe.msgprint(__("Import completed. {0} common codes created.", [count]));
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
|
||||
d.fields_dict.code_column.df.onchange = () => update_preview(d, context);
|
||||
d.fields_dict.title_column.df.onchange = (e) => {
|
||||
let field = d.fields_dict.title_column;
|
||||
if (!e.target.value) {
|
||||
field.df.description = title_description;
|
||||
field.refresh();
|
||||
} else {
|
||||
field.df.description = null;
|
||||
field.refresh();
|
||||
}
|
||||
update_preview(d, context);
|
||||
};
|
||||
|
||||
// Add onchange events for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context);
|
||||
}
|
||||
|
||||
d.show();
|
||||
update_preview(d, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first key from the keys array that is found in the columns array.
|
||||
*/
|
||||
function get_default(columns, keys) {
|
||||
return keys.find((key) => columns.includes(key));
|
||||
}
|
||||
|
||||
function update_preview(dialog, context) {
|
||||
let code_column = dialog.get_value("code_column");
|
||||
let title_column = dialog.get_value("title_column");
|
||||
let description_column = dialog.get_value("description_column");
|
||||
|
||||
let html = '<table class="table table-bordered"><thead><tr>';
|
||||
if (title_column) html += `<th>${__("Title")}</th>`;
|
||||
if (code_column) html += `<th>${__("Code")}</th>`;
|
||||
if (description_column) html += `<th>${__("Description")}</th>`;
|
||||
|
||||
// Add headers for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
html += `<th>${__(column)}</th>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr></thead><tbody>";
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
html += "<tr>";
|
||||
if (title_column) {
|
||||
let title = context.example_values[title_column][i] || "";
|
||||
html += `<td title="${title}">${truncate(title)}</td>`;
|
||||
}
|
||||
if (code_column) {
|
||||
let code = context.example_values[code_column][i] || "";
|
||||
html += `<td title="${code}">${truncate(code)}</td>`;
|
||||
}
|
||||
if (description_column) {
|
||||
let description = context.example_values[description_column][i] || "";
|
||||
html += `<td title="${description}">${truncate(description)}</td>`;
|
||||
}
|
||||
|
||||
// Add values for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
let value = context.example_values[column][i] || "";
|
||||
html += `<td title="${value}">${truncate(value)}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr>";
|
||||
}
|
||||
|
||||
html += "</tbody></table>";
|
||||
|
||||
dialog.fields_dict.preview_html.$wrapper.html(html);
|
||||
}
|
||||
|
||||
function truncate(value, maxLength = 40) {
|
||||
if (typeof value !== "string") return "";
|
||||
return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value;
|
||||
}
|
||||
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from lxml import etree
|
||||
|
||||
URL_PREFIXES = ("http://", "https://")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_genericode():
|
||||
doctype = "Code List"
|
||||
docname = frappe.form_dict.docname
|
||||
content = frappe.local.uploaded_file
|
||||
|
||||
# recover the content, if it's a link
|
||||
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
|
||||
try:
|
||||
# If it's a URL, fetch the content and make it a local file (for durable audit)
|
||||
response = requests.get(frappe.local.uploaded_file_url)
|
||||
response.raise_for_status()
|
||||
frappe.local.uploaded_file = content = response.content
|
||||
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
|
||||
frappe.local.uploaded_file_url = None
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
|
||||
|
||||
if file_url := frappe.local.uploaded_file_url:
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_url)
|
||||
with open(file_path.encode(), mode="rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the xml content
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
try:
|
||||
root = etree.fromstring(content, parser=parser)
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
|
||||
|
||||
# Extract the name (CanonicalVersionUri) from the parsed XML
|
||||
name = root.find(".//CanonicalVersionUri").text
|
||||
docname = docname or name
|
||||
|
||||
if frappe.db.exists(doctype, docname):
|
||||
code_list = frappe.get_doc(doctype, docname)
|
||||
if code_list.name != name:
|
||||
frappe.throw(_("The uploaded file does not match the selected Code List."))
|
||||
else:
|
||||
# Create a new Code List document with the extracted name
|
||||
code_list = frappe.new_doc(doctype)
|
||||
code_list.name = name
|
||||
|
||||
code_list.from_genericode(root)
|
||||
code_list.save()
|
||||
|
||||
# Attach the file and provide a recoverable identifier
|
||||
file_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"attached_to_doctype": "Code List",
|
||||
"attached_to_name": code_list.name,
|
||||
"folder": "Home/Attachments",
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
"content": content,
|
||||
}
|
||||
).save()
|
||||
|
||||
# Get available columns and example values
|
||||
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||
|
||||
return {
|
||||
"code_list": code_list.name,
|
||||
"code_list_title": code_list.title,
|
||||
"file": file_doc.name,
|
||||
"columns": columns,
|
||||
"example_values": example_values,
|
||||
"filterable_columns": filterable_columns,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_genericode_import(
|
||||
code_list_name: str,
|
||||
file_name: str,
|
||||
code_column: str,
|
||||
title_column: str | None = None,
|
||||
description_column: str | None = None,
|
||||
filters: str | None = None,
|
||||
):
|
||||
from erpnext.edi.doctype.common_code.common_code import import_genericode
|
||||
|
||||
column_map = {"code": code_column, "title": title_column, "description": description_column}
|
||||
|
||||
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
|
||||
|
||||
|
||||
def get_genericode_columns_and_examples(root):
|
||||
columns = []
|
||||
example_values = {}
|
||||
filterable_columns = {}
|
||||
|
||||
# Get column names
|
||||
for column in root.findall(".//Column"):
|
||||
column_id = column.get("Id")
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
# Get all values and count unique occurrences
|
||||
for row in root.findall(".//SimpleCodeList/Row"):
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
if column_id not in columns:
|
||||
# Handle undeclared column
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
filterable_columns[column_id].add(simple_value.text)
|
||||
|
||||
# Get example values (up to 3) and filter columns with cardinality <= 5
|
||||
for row in root.findall(".//SimpleCodeList/Row")[:3]:
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
example_values[column_id].append(simple_value.text)
|
||||
|
||||
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
|
||||
|
||||
return columns, example_values, filterable_columns
|
||||
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Code List"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCodeList(FrappeTestCase):
|
||||
pass
|
||||
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Common Code", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2024-09-29 07:01:18.133067",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
"column_break_wxsw",
|
||||
"additional_data",
|
||||
"section_break_rhgh",
|
||||
"applies_to"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "code_list",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Code List",
|
||||
"options": "Code List",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Title",
|
||||
"length": 300,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wxsw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rhgh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "applies_to",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applies To",
|
||||
"options": "Dynamic Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "common_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Common Code",
|
||||
"length": 300,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_data",
|
||||
"fieldtype": "Code",
|
||||
"label": "Additional Data",
|
||||
"max_height": "190px",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class CommonCode(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
|
||||
from frappe.types import DF
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
title: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_distinct_references()
|
||||
|
||||
def validate_distinct_references(self):
|
||||
"""Ensure no two Common Codes of the same Code List are linked to the same document."""
|
||||
for link in self.applies_to:
|
||||
existing_links = frappe.get_all(
|
||||
"Common Code",
|
||||
filters=[
|
||||
["name", "!=", self.name],
|
||||
["code_list", "=", self.code_list],
|
||||
["Dynamic Link", "link_doctype", "=", link.link_doctype],
|
||||
["Dynamic Link", "link_name", "=", link.link_name],
|
||||
],
|
||||
fields=["name", "common_code"],
|
||||
)
|
||||
|
||||
if existing_links:
|
||||
existing_link = existing_links[0]
|
||||
frappe.throw(
|
||||
_("{0} {1} is already linked to Common Code {2}.").format(
|
||||
link.link_doctype,
|
||||
link.link_name,
|
||||
get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
|
||||
)
|
||||
)
|
||||
|
||||
def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
|
||||
"""Populate the Common Code document from a genericode XML element
|
||||
|
||||
Args:
|
||||
column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
|
||||
code (etree.Element): The XML element representing a code in the genericode file
|
||||
"""
|
||||
title_column = column_map.get("title")
|
||||
code_column = column_map["code"]
|
||||
description_column = column_map.get("description")
|
||||
|
||||
self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
|
||||
|
||||
if title_column:
|
||||
simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
|
||||
self.title = simple_value_title.text if simple_value_title is not None else self.common_code
|
||||
|
||||
if description_column:
|
||||
simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
|
||||
self.description = simple_value_descr.text if simple_value_descr is not None else None
|
||||
|
||||
self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
|
||||
|
||||
|
||||
def simple_hash(input_string, length=6):
|
||||
return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
|
||||
|
||||
|
||||
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
|
||||
"""Import genericode file and create Common Code entries"""
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_name)
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser=parser)
|
||||
root = tree.getroot()
|
||||
|
||||
# Construct the XPath expression
|
||||
xpath_expr = ".//SimpleCodeList/Row"
|
||||
filter_conditions = [
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||
]
|
||||
if filter_conditions:
|
||||
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||
|
||||
elements = root.xpath(xpath_expr)
|
||||
total_elements = len(elements)
|
||||
for i, xml_element in enumerate(elements, start=1):
|
||||
common_code: "CommonCode" = frappe.new_doc("Common Code")
|
||||
common_code.code_list = code_list
|
||||
common_code.from_genericode(column_map, xml_element)
|
||||
common_code.save()
|
||||
frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
|
||||
|
||||
return total_elements
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Common Code", ["code_list", "common_code"])
|
||||
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Common Code"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCommonCode(FrappeTestCase):
|
||||
pass
|
||||
@@ -1,211 +1,78 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "field:gateway_name",
|
||||
"beta": 0,
|
||||
"creation": "2018-02-06 16:11:10.028249",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"gateway_name",
|
||||
"section_break_2",
|
||||
"access_token",
|
||||
"webhooks_secret",
|
||||
"use_sandbox",
|
||||
"header_img"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway_name",
|
||||
"fieldtype": "Data",
|
||||
"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": "Payment Gateway Name",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section 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
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Data",
|
||||
"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": "Access Token",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "webhooks_secret",
|
||||
"fieldtype": "Data",
|
||||
"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": "Webhooks Secret",
|
||||
"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
|
||||
"label": "Webhooks Secret"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "use_sandbox",
|
||||
"fieldtype": "Check",
|
||||
"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": "Use Sandbox",
|
||||
"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
|
||||
"label": "Use Sandbox"
|
||||
},
|
||||
{
|
||||
"fieldname": "header_img",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Header Image"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2022-02-12 14:18:47.209114",
|
||||
"links": [],
|
||||
"modified": "2024-07-22 12:34:26.791274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "GoCardless Settings",
|
||||
"name_case": "",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -25,7 +25,12 @@ frappe.ui.form.on("Plaid Settings", {
|
||||
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
|
||||
let bank_transaction_link = frappe.utils.get_form_link(
|
||||
"Bank Transaction",
|
||||
"",
|
||||
true,
|
||||
"Bank Transaction"
|
||||
);
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Sync Started"),
|
||||
|
||||
@@ -25,6 +25,14 @@ doctype_js = {
|
||||
"Newsletter": "public/js/newsletter.js",
|
||||
"Contact": "public/js/contact.js",
|
||||
}
|
||||
doctype_list_js = {
|
||||
"Code List": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
"Common Code": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
}
|
||||
|
||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ frappe.ui.form.on("BOM", {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
item_code: d.item_code,
|
||||
variant_item_code: "",
|
||||
qty: d.qty,
|
||||
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
|
||||
source_warehouse: d.source_warehouse,
|
||||
operation: d.operation,
|
||||
});
|
||||
|
||||
@@ -193,6 +193,24 @@ class BOM(WebsiteGenerator):
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||
self.set_process_loss_qty()
|
||||
self.validate_scrap_items()
|
||||
self.set_default_uom()
|
||||
|
||||
def set_default_uom(self):
|
||||
if not self.get("items"):
|
||||
return
|
||||
|
||||
item_wise_uom = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item",
|
||||
filters={"name": ("in", [item.item_code for item in self.items])},
|
||||
fields=["name", "stock_uom"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
for row in self.get("items"):
|
||||
if row.stock_uom != item_wise_uom.get(row.item_code):
|
||||
row.stock_uom = item_wise_uom.get(row.item_code)
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{"name": "boms", "title": _("All BOMs")}]
|
||||
|
||||
@@ -755,6 +755,26 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
|
||||
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
|
||||
|
||||
def test_bom_raw_materials_stock_uom(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"}
|
||||
).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True)
|
||||
for row in bom.items:
|
||||
self.assertEqual(row.stock_uom, "Nos")
|
||||
|
||||
frappe.db.set_value("Item", rm_item, "stock_uom", "Kg")
|
||||
|
||||
bom.items[0].qty = 2
|
||||
bom.save()
|
||||
|
||||
for row in bom.items:
|
||||
self.assertEqual(row.stock_uom, "Kg")
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, get_datetime, now
|
||||
|
||||
|
||||
class BOMUpdateTool(Document):
|
||||
@@ -38,13 +39,21 @@ def auto_update_latest_price_in_all_boms() -> None:
|
||||
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
|
||||
wip_log = frappe.get_all(
|
||||
"BOM Update Log",
|
||||
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
fields=["creation", "status"],
|
||||
filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
limit_page_length=1,
|
||||
order_by="creation desc",
|
||||
)
|
||||
if not wip_log:
|
||||
|
||||
if not wip_log or is_older_log(wip_log[0]):
|
||||
create_bom_update_log(update_type="Update Cost")
|
||||
|
||||
|
||||
def is_older_log(log: dict) -> bool:
|
||||
no_of_days = date_diff(get_datetime(now()), get_datetime(log.creation))
|
||||
return no_of_days > 10
|
||||
|
||||
|
||||
def create_bom_update_log(
|
||||
boms: dict[str, str] | None = None,
|
||||
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
@@ -74,4 +74,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
|
||||
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
|
||||
"label": "Set Operating Cost / Scrap Items From Sub-assemblies"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
@@ -226,4 +226,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +89,18 @@ class WorkOrder(Document):
|
||||
self.status = self.get_status()
|
||||
self.validate_workstation_type()
|
||||
|
||||
if self.source_warehouse:
|
||||
self.set_warehouses()
|
||||
|
||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
|
||||
def set_warehouses(self):
|
||||
for row in self.required_items:
|
||||
if not row.source_warehouse:
|
||||
row.source_warehouse = self.source_warehouse
|
||||
|
||||
def validate_workstation_type(self):
|
||||
for row in self.operations:
|
||||
if not row.workstation and not row.workstation_type:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"include_item_in_manufacturing",
|
||||
"qty_section",
|
||||
"required_qty",
|
||||
"stock_uom",
|
||||
"rate",
|
||||
"amount",
|
||||
"column_break_11",
|
||||
@@ -138,11 +139,19 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Returned Qty ",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-11 15:45:32.318374",
|
||||
"modified": "2024-11-19 15:48:16.823384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Item",
|
||||
@@ -153,4 +162,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ def get_chart_data(periodic_data, columns):
|
||||
pending.append(periodic_data.get("Pending").get(d))
|
||||
completed.append(periodic_data.get("Completed").get(d))
|
||||
|
||||
datasets.append({"name": "All Work Orders", "values": all_data})
|
||||
datasets.append({"name": "Not Started", "values": not_start})
|
||||
datasets.append({"name": "Overdue", "values": overdue})
|
||||
datasets.append({"name": "Pending", "values": pending})
|
||||
datasets.append({"name": "Completed", "values": completed})
|
||||
datasets.append({"name": _("All Work Orders"), "values": all_data})
|
||||
datasets.append({"name": _("Not Started"), "values": not_start})
|
||||
datasets.append({"name": _("Overdue"), "values": overdue})
|
||||
datasets.append({"name": _("Pending"), "values": pending})
|
||||
datasets.append({"name": _("Completed"), "values": completed})
|
||||
|
||||
chart = {"data": {"labels": labels, "datasets": datasets}}
|
||||
chart["type"] = "line"
|
||||
|
||||
@@ -42,7 +42,7 @@ def get_data(filters):
|
||||
|
||||
|
||||
def get_chart_data(periodic_data, columns):
|
||||
labels = ["Rejected", "Accepted"]
|
||||
labels = [_("Rejected"), _("Accepted")]
|
||||
|
||||
status_wise_data = {"Accepted": 0, "Rejected": 0}
|
||||
|
||||
@@ -53,7 +53,7 @@ def get_chart_data(periodic_data, columns):
|
||||
|
||||
datasets.append(
|
||||
{
|
||||
"name": "Qty Wise Chart",
|
||||
"name": _("Qty Wise Chart"),
|
||||
"values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,61 +4,67 @@ import frappe
|
||||
|
||||
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||
|
||||
DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
|
||||
class TestManufacturingReports(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.setup_default_filters()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def setup_default_filters(self):
|
||||
self.last_bom = frappe.get_last_doc("BOM").name
|
||||
self.DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": self.last_bom}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
self.REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
self.OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
def test_execute_all_manufacturing_reports(self):
|
||||
"""Test that all script report in manufacturing modules are executable with supported filters"""
|
||||
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||
for report, filter in self.REPORT_FILTER_TEST_CASES:
|
||||
with self.subTest(report=report):
|
||||
execute_script_report(
|
||||
report_name=report,
|
||||
module="Manufacturing",
|
||||
filters=filter,
|
||||
default_filters=DEFAULT_FILTERS,
|
||||
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
default_filters=self.DEFAULT_FILTERS,
|
||||
optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
)
|
||||
|
||||
@@ -50,7 +50,11 @@ def get_returned_materials(work_orders):
|
||||
|
||||
raw_materials = frappe.get_all(
|
||||
"Stock Entry",
|
||||
fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"],
|
||||
fields=[
|
||||
"`tabStock Entry`.`work_order`",
|
||||
"`tabStock Entry Detail`.`item_code`",
|
||||
"`tabStock Entry Detail`.`qty`",
|
||||
],
|
||||
filters=[
|
||||
["Stock Entry", "is_return", "=", 1],
|
||||
["Stock Entry Detail", "docstatus", "=", 1],
|
||||
@@ -59,12 +63,14 @@ def get_returned_materials(work_orders):
|
||||
)
|
||||
|
||||
for d in raw_materials:
|
||||
raw_materials_qty[d.item_code] += d.qty
|
||||
key = (d.work_order, d.item_code)
|
||||
raw_materials_qty[key] += d.qty
|
||||
|
||||
for row in work_orders:
|
||||
row.returned_qty = 0.0
|
||||
if raw_materials_qty.get(row.raw_material_item_code):
|
||||
row.returned_qty = raw_materials_qty.get(row.raw_material_item_code)
|
||||
key = (row.parent, row.raw_material_item_code)
|
||||
if raw_materials_qty.get(key):
|
||||
row.returned_qty = raw_materials_qty.get(key)
|
||||
|
||||
|
||||
def get_fields():
|
||||
|
||||
@@ -19,4 +19,5 @@ Loan Management
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
Subcontracting
|
||||
Subcontracting
|
||||
EDI
|
||||
|
||||
@@ -367,3 +367,4 @@ erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
|
||||
@@ -38,7 +38,7 @@ def execute():
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company, creation
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
@@ -67,6 +67,7 @@ def execute():
|
||||
"voucher_type": d.voucher_type,
|
||||
"voucher_no": d.voucher_no,
|
||||
"sle_id": d.name,
|
||||
"creation": d.creation,
|
||||
},
|
||||
allow_negative_stock=True,
|
||||
)
|
||||
|
||||
15
erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py
Normal file
15
erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE
|
||||
`tabWork Order Item`, `tabItem`
|
||||
SET
|
||||
`tabWork Order Item`.stock_uom = `tabItem`.stock_uom
|
||||
WHERE
|
||||
`tabWork Order Item`.item_code = `tabItem`.name
|
||||
AND `tabWork Order Item`.docstatus = 1
|
||||
"""
|
||||
)
|
||||
@@ -190,7 +190,7 @@ frappe.ui.form.on("Project", {
|
||||
},
|
||||
|
||||
set_status: function (frm, status) {
|
||||
frappe.confirm(__("Set Project and all Tasks to status {0}?", [status.bold()]), () => {
|
||||
frappe.confirm(__("Set Project and all Tasks to status {0}?", [__(status).bold()]), () => {
|
||||
frappe
|
||||
.xcall("erpnext.projects.doctype.project.project.set_project_status", {
|
||||
project: frm.doc.name,
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
||||
from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, nowtime, today
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.controllers.queries import get_filters_cond
|
||||
@@ -275,24 +275,19 @@ class Project(Document):
|
||||
frappe.db.set_value("Project", new_name, "copied_from", new_name)
|
||||
|
||||
def send_welcome_email(self):
|
||||
url = get_url(f"/project/?name={self.name}")
|
||||
messages = (
|
||||
_("You have been invited to collaborate on the project: {0}").format(self.name),
|
||||
url,
|
||||
_("Join"),
|
||||
)
|
||||
label = f"{self.project_name} ({self.name})"
|
||||
url = get_link_to_form(self.doctype, self.name, label)
|
||||
|
||||
content = """
|
||||
<p>{0}.</p>
|
||||
<p><a href="{1}">{2}</a></p>
|
||||
"""
|
||||
content = "<p>{}</p>".format(
|
||||
_("You have been invited to collaborate on the project: {0}").format(url)
|
||||
)
|
||||
|
||||
for user in self.users:
|
||||
if user.welcome_email_sent == 0:
|
||||
frappe.sendmail(
|
||||
user.user,
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content.format(*messages),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class Timesheet(Document):
|
||||
if data.task and data.task not in tasks:
|
||||
task = frappe.get_doc("Task", data.task)
|
||||
task.update_time_and_costing()
|
||||
task.save()
|
||||
task.save(ignore_permissions=True)
|
||||
tasks.append(data.task)
|
||||
|
||||
elif data.project and data.project not in projects:
|
||||
|
||||
@@ -73,8 +73,8 @@ def get_chart_data(data):
|
||||
on_track = on_track + 1
|
||||
charts = {
|
||||
"data": {
|
||||
"labels": ["On Track", "Delayed"],
|
||||
"datasets": [{"name": "Delayed", "values": [on_track, delay]}],
|
||||
"labels": [_("On Track"), _("Delayed")],
|
||||
"datasets": [{"name": _("Delayed"), "values": [on_track, delay]}],
|
||||
},
|
||||
"type": "percentage",
|
||||
"colors": ["#84D5BA", "#CB4B5F"],
|
||||
|
||||
@@ -828,13 +828,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
);
|
||||
}
|
||||
|
||||
if(!this.frm.doc.is_return){
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
} else {
|
||||
payment.amount = 0
|
||||
}
|
||||
});
|
||||
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
setup_sms() {
|
||||
var me = this;
|
||||
let blacklist = ['Purchase Invoice', 'BOM'];
|
||||
if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
|
||||
if(frappe.boot.sms_gateway_enabled && this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
|
||||
&& !blacklist.includes(this.frm.doctype)) {
|
||||
this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); });
|
||||
}
|
||||
@@ -990,7 +990,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
apply_discount_on_item(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.price_list_rate) {
|
||||
if(item && !item.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
@@ -1104,6 +1104,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
|
||||
me.apply_price_list(item, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,40 +9,29 @@ erpnext.financial_statements = {
|
||||
data &&
|
||||
column.colIndex >= 3
|
||||
) {
|
||||
//Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns.
|
||||
const lastAnnualValue = row[column.colIndex - 1].content;
|
||||
const currentAnnualvalue = data[column.fieldname];
|
||||
if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
let annualGrowth = 0;
|
||||
if (lastAnnualValue == 0 && currentAnnualvalue > 0) {
|
||||
//If the previous year value is 0 and the current value is greater than 0
|
||||
annualGrowth = 1;
|
||||
} else if (lastAnnualValue > 0) {
|
||||
annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue;
|
||||
}
|
||||
const growthPercent = data[column.fieldname];
|
||||
|
||||
const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage
|
||||
if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
|
||||
value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`);
|
||||
if (growthPercent < 0) {
|
||||
value = $(value).addClass("text-danger");
|
||||
if (column.fieldname === "total") {
|
||||
value = $(`<span>${growthPercent}</span>`);
|
||||
} else {
|
||||
value = $(value).addClass("text-success");
|
||||
value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`);
|
||||
|
||||
if (growthPercent < 0) {
|
||||
value = $(value).addClass("text-danger");
|
||||
} else {
|
||||
value = $(value).addClass("text-success");
|
||||
}
|
||||
}
|
||||
value = $(value).wrap("<p></p>").parent().html();
|
||||
|
||||
return value;
|
||||
} else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) {
|
||||
if (column.fieldname == "account" && data.account_name == __("Income")) {
|
||||
//Taking the total income from each column (for all the financial years) as the base (100%)
|
||||
this.baseData = row;
|
||||
}
|
||||
if (column.colIndex >= 2) {
|
||||
//Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns.
|
||||
const currentAnnualvalue = data[column.fieldname];
|
||||
const baseValue = this.baseData[column.colIndex].content;
|
||||
if (currentAnnualvalue == undefined || baseValue <= 0) return "NA";
|
||||
const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100;
|
||||
const marginPercent = data[column.fieldname];
|
||||
|
||||
if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
|
||||
value = $(`<span>${marginPercent + "%"}</span>`);
|
||||
if (marginPercent < 0) value = $(value).addClass("text-danger");
|
||||
|
||||
@@ -64,6 +64,17 @@ $.extend(erpnext.queries, {
|
||||
}
|
||||
},
|
||||
|
||||
company_contact_query: function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))]));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.contact.contact.contact_query",
|
||||
filters: { link_doctype: "Company", link_name: doc.company },
|
||||
};
|
||||
},
|
||||
|
||||
address_query: function (doc) {
|
||||
if (frappe.dynamic_link) {
|
||||
if (!doc[frappe.dynamic_link.fieldname]) {
|
||||
@@ -85,16 +96,23 @@ $.extend(erpnext.queries, {
|
||||
},
|
||||
|
||||
company_address_query: function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set {0}", [frappe.meta.get_label(doc.doctype, "company", doc.name)]));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" },
|
||||
filters: { link_doctype: "Company", link_name: doc.company },
|
||||
};
|
||||
},
|
||||
|
||||
dispatch_address_query: function (doc) {
|
||||
var filters = { link_doctype: "Company", link_name: doc.company || "" };
|
||||
var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
|
||||
if (is_drop_ship) filters = {};
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: { link_doctype: "Company", link_name: doc.company || "" },
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user