mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 04:42:40 +00:00
Compare commits
337 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2ac603fd7 | ||
|
|
3340b19c5d | ||
|
|
4d1022182d | ||
|
|
f2b9e73819 | ||
|
|
31cb78e3f3 | ||
|
|
789cd20d2d | ||
|
|
cb58d05777 | ||
|
|
ad5421a413 | ||
|
|
84f9a1fb2d | ||
|
|
7d88b8bbb9 | ||
|
|
baa08ce496 | ||
|
|
7031a5033c | ||
|
|
1f1e88d52e | ||
|
|
97f13049ee | ||
|
|
1c9032a4c2 | ||
|
|
323a91ddd7 | ||
|
|
ee30357835 | ||
|
|
4d18fd0e80 | ||
|
|
d054486495 | ||
|
|
e9365d7272 | ||
|
|
b3578ecf09 | ||
|
|
23db22ac0b | ||
|
|
85099a3a68 | ||
|
|
db9f0b6f38 | ||
|
|
56311e6e1e | ||
|
|
40504b8da2 | ||
|
|
379def5f0f | ||
|
|
ab6a534df2 | ||
|
|
7b5e2a6af0 | ||
|
|
994454bfc3 | ||
|
|
9febca2981 | ||
|
|
62efd09f2f | ||
|
|
b8f1c8f6b1 | ||
|
|
f92b5b9a2e | ||
|
|
dd39d24da0 | ||
|
|
dc8bb792d7 | ||
|
|
bd11146f02 | ||
|
|
33f1d7a5fe | ||
|
|
60de0474a1 | ||
|
|
8156d89903 | ||
|
|
66b0426155 | ||
|
|
60b12b8319 | ||
|
|
112d40db22 | ||
|
|
0d0b05bf6c | ||
|
|
c2d7e8c471 | ||
|
|
ccb0f7ac42 | ||
|
|
781b66e252 | ||
|
|
efd3b1c966 | ||
|
|
795108c1dd | ||
|
|
7348778220 | ||
|
|
eaeb18c651 | ||
|
|
78607b5812 | ||
|
|
823cfeaf4f | ||
|
|
aa29c5dde2 | ||
|
|
713b17c3a5 | ||
|
|
23b5d2db2c | ||
|
|
84b2f871ba | ||
|
|
ac78bfb55b | ||
|
|
59dd5fee26 | ||
|
|
35035c2a31 | ||
|
|
78c63869e0 | ||
|
|
06ea957ae5 | ||
|
|
f27e591d88 | ||
|
|
d24c2c4cca | ||
|
|
5237ff8d94 | ||
|
|
b3eb49d39d | ||
|
|
039c47e3f2 | ||
|
|
7ecb4d3d6f | ||
|
|
76a9e45ff8 | ||
|
|
c69a0f150d | ||
|
|
66f41d44c4 | ||
|
|
7393a9f470 | ||
|
|
43d4e26ac5 | ||
|
|
9167d2ef64 | ||
|
|
f88e68230a | ||
|
|
3aee14176c | ||
|
|
d6796da464 | ||
|
|
9a47c507c0 | ||
|
|
95d1d7047d | ||
|
|
a2cdd91a0d | ||
|
|
617b0658b8 | ||
|
|
d8e9ed417d | ||
|
|
eb7eadc16f | ||
|
|
d21bfa219d | ||
|
|
198089cac1 | ||
|
|
d05b49b0f8 | ||
|
|
f490de9285 | ||
|
|
a5e5553520 | ||
|
|
ea393ef008 | ||
|
|
ef77791bd6 | ||
|
|
63d165c48a | ||
|
|
129cd7ae8a | ||
|
|
83a57909d3 | ||
|
|
29d7593fa7 | ||
|
|
a8e1c4f6cd | ||
|
|
5a5449c60c | ||
|
|
6d2c14c75e | ||
|
|
2e78e14c7e | ||
|
|
996fb7552a | ||
|
|
04349b61bd | ||
|
|
d3202068d9 | ||
|
|
76c2477d23 | ||
|
|
1b1550708d | ||
|
|
425674e164 | ||
|
|
e98ad4ce27 | ||
|
|
0314a39fab | ||
|
|
e5d06f8c86 | ||
|
|
fb5d60eeb6 | ||
|
|
8d7be8a536 | ||
|
|
b99f8fd021 | ||
|
|
8b4824fef5 | ||
|
|
916511ef1a | ||
|
|
c614ff419b | ||
|
|
d5163ed502 | ||
|
|
5bac652b5f | ||
|
|
3582b32f03 | ||
|
|
2815a0d827 | ||
|
|
7d828dc17e | ||
|
|
01dd7337a2 | ||
|
|
470534af78 | ||
|
|
a4fe89f65c | ||
|
|
4a1966c680 | ||
|
|
cd7462dd87 | ||
|
|
3c697e90a3 | ||
|
|
6dde327713 | ||
|
|
16b10274cf | ||
|
|
4c1b415b9d | ||
|
|
d7124779bf | ||
|
|
053a5b93ca | ||
|
|
9f5cfdd65b | ||
|
|
5ebf1b9cc4 | ||
|
|
34b62d226c | ||
|
|
9bf8904c80 | ||
|
|
ba8a316b06 | ||
|
|
3879cbd86d | ||
|
|
9df3b9b059 | ||
|
|
0131800df2 | ||
|
|
2e3ebec53c | ||
|
|
ef9ccd7a57 | ||
|
|
903d9b50fe | ||
|
|
0c612be6fe | ||
|
|
893a86e173 | ||
|
|
5a23d7cdca | ||
|
|
8623a5650b | ||
|
|
73bc57f53e | ||
|
|
9192913832 | ||
|
|
f59093c6b7 | ||
|
|
7ede5392bd | ||
|
|
0f5c7d95a0 | ||
|
|
f783bf60a4 | ||
|
|
4c49ab19d6 | ||
|
|
de1afee75a | ||
|
|
db97dbd394 | ||
|
|
fb2df77da2 | ||
|
|
260073f14a | ||
|
|
92d5e91e1f | ||
|
|
4fd1af2118 | ||
|
|
5997e37454 | ||
|
|
63ba27e359 | ||
|
|
fd19f284c4 | ||
|
|
74e29f1218 | ||
|
|
5dee1d40ac | ||
|
|
895231a8a7 | ||
|
|
8ed6e98565 | ||
|
|
70f9c13f3c | ||
|
|
a2f5975133 | ||
|
|
d3d22f699e | ||
|
|
60dfe36195 | ||
|
|
b9698366c3 | ||
|
|
1fb4ac44fe | ||
|
|
0f2264658f | ||
|
|
fe78bb60c4 | ||
|
|
194e41a2d9 | ||
|
|
2c8db092a0 | ||
|
|
c44493fd7e | ||
|
|
01b0d1057e | ||
|
|
9d2f396d75 | ||
|
|
b3cbbf2ce3 | ||
|
|
0cbb7223be | ||
|
|
c09b258d57 | ||
|
|
d5d1a51b92 | ||
|
|
9abac4c6df | ||
|
|
48f786e493 | ||
|
|
a137944955 | ||
|
|
bb54bebe94 | ||
|
|
ba009f4626 | ||
|
|
e05888502f | ||
|
|
2961e595c2 | ||
|
|
f41bcc6fec | ||
|
|
a6b1bdc78b | ||
|
|
f17b7b5ee9 | ||
|
|
5529a17831 | ||
|
|
62aa1cdb33 | ||
|
|
3d2d1ba072 | ||
|
|
f3bc80c89d | ||
|
|
6892994ef6 | ||
|
|
2c22615b6b | ||
|
|
cd1c10a43f | ||
|
|
db318a4e9b | ||
|
|
9a78283ecb | ||
|
|
52a5cd9702 | ||
|
|
f3052a446f | ||
|
|
ce9da48a5e | ||
|
|
a450f4ce64 | ||
|
|
a029f2e8a3 | ||
|
|
32eeedac24 | ||
|
|
ecb2bab70f | ||
|
|
cf354c0da3 | ||
|
|
73fc8c374f | ||
|
|
8c4ce03f44 | ||
|
|
69337cf18b | ||
|
|
b773b494a0 | ||
|
|
70e190dbbb | ||
|
|
be280a408e | ||
|
|
9584c80857 | ||
|
|
1a69d8137f | ||
|
|
35c7af1b9d | ||
|
|
da4ed5cc18 | ||
|
|
927d0f686f | ||
|
|
8ee9a46d96 | ||
|
|
86aa072235 | ||
|
|
72ae80e2e3 | ||
|
|
829550cd99 | ||
|
|
249d18962c | ||
|
|
b9f12ed4c7 | ||
|
|
80c76618ae | ||
|
|
073d06c44f | ||
|
|
612fa7c672 | ||
|
|
e7dc31191c | ||
|
|
926c0c5cf4 | ||
|
|
52cab02a5c | ||
|
|
31fa1c9a58 | ||
|
|
631a9bfa7c | ||
|
|
67741f1a21 | ||
|
|
f225e1986e | ||
|
|
9355782397 | ||
|
|
30ec69c977 | ||
|
|
b22831bd94 | ||
|
|
5a3eff05a1 | ||
|
|
09a46fcf0e | ||
|
|
37f4cf5367 | ||
|
|
f95a3f5b8b | ||
|
|
0286788e97 | ||
|
|
8891f46a22 | ||
|
|
890ce4a676 | ||
|
|
2960d0dce1 | ||
|
|
6e699178ae | ||
|
|
0f350ef24d | ||
|
|
56d0357f6f | ||
|
|
5b50d5abf2 | ||
|
|
9cede83de1 | ||
|
|
a8b982dd0a | ||
|
|
cf45ffdabe | ||
|
|
e91a0acbb3 | ||
|
|
fbbae80f92 | ||
|
|
dcfae61a7a | ||
|
|
3deb11e5b2 | ||
|
|
242a119f95 | ||
|
|
a75931c90f | ||
|
|
96d3bfd2d9 | ||
|
|
165a4fcef6 | ||
|
|
29b35d6eb0 | ||
|
|
39c029133f | ||
|
|
7f55d59a7b | ||
|
|
95f21e5ecd | ||
|
|
bafd9ed15e | ||
|
|
25fabda40a | ||
|
|
b6e5e3347d | ||
|
|
2868446292 | ||
|
|
811fe4fee6 | ||
|
|
b6bf13ff02 | ||
|
|
b82e2585d5 | ||
|
|
9ca96a63c3 | ||
|
|
98cb9c6b96 | ||
|
|
d61a85e316 | ||
|
|
6dbdc36af9 | ||
|
|
b18692c120 | ||
|
|
309ea7b9cf | ||
|
|
775b2432ab | ||
|
|
64ae4e1fec | ||
|
|
005014ef6a | ||
|
|
4a37f2a925 | ||
|
|
870be7a79b | ||
|
|
67d24e9635 | ||
|
|
3ba7bb3ab7 | ||
|
|
72ec3f3d18 | ||
|
|
7ba7d1a2a4 | ||
|
|
7abe199e2a | ||
|
|
09e7bfbacb | ||
|
|
ef7b09fc11 | ||
|
|
0adb7156cd | ||
|
|
7fb557197a | ||
|
|
7733e417a4 | ||
|
|
d8fb11009f | ||
|
|
2bd30e3c46 | ||
|
|
fb56db1166 | ||
|
|
b6908a79bd | ||
|
|
f4551bb918 | ||
|
|
01e975b481 | ||
|
|
91cbf2ec4f | ||
|
|
c2e36daa32 | ||
|
|
74caf8134c | ||
|
|
40faa7f7b9 | ||
|
|
f73e99e9d2 | ||
|
|
2d77e056bc | ||
|
|
0aabe4fd1e | ||
|
|
f94a14c06a | ||
|
|
d9636018f5 | ||
|
|
2d96a62530 | ||
|
|
eba73df88e | ||
|
|
c19065e675 | ||
|
|
f8fa775af3 | ||
|
|
91e167fe72 | ||
|
|
33366fce6c | ||
|
|
8d1e855dc8 | ||
|
|
0e5c709f7b | ||
|
|
21612fc230 | ||
|
|
afb44a677c | ||
|
|
4f6aee3f22 | ||
|
|
fc1b1ca5e2 | ||
|
|
f69b8d7e2d | ||
|
|
2147441e64 | ||
|
|
5e5cf68b32 | ||
|
|
7c8245d299 | ||
|
|
a4c9707fdf | ||
|
|
f4a43d07b0 | ||
|
|
c1ed750bcb | ||
|
|
1d139eb94a | ||
|
|
73a418a2bd | ||
|
|
035394ae6a | ||
|
|
1ef7da837f | ||
|
|
85a8adf804 | ||
|
|
d3445b3079 | ||
|
|
ada7821a49 | ||
|
|
3f7dcedf3d | ||
|
|
4fc14b3097 | ||
|
|
4ef2b77973 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.60.0"
|
||||
__version__ = "15.65.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -38,8 +38,14 @@
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -77,9 +83,12 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
@@ -532,6 +541,65 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_internal_transaction_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_internal_transaction_rate",
|
||||
"fieldname": "maintain_same_rate_action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
|
||||
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||
"fieldname": "role_to_override_stop_action",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_price_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Price Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Pegged Currencies Exchange Rates"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -539,7 +607,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified": "2025-06-16 16:40:54.871486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -568,4 +636,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class AccountsSettings(Document):
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
@@ -50,13 +51,17 @@ class AccountsSettings(Document):
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
maintain_same_internal_transaction_rate: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
|
||||
@@ -7,6 +7,9 @@ import frappe
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -191,11 +194,13 @@ def make_pos_sales_invoice():
|
||||
|
||||
customer = make_customer(customer="_Test Customer")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
|
||||
|
||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si.set("payments", [])
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
||||
)
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ def get_import_status(docname):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_logs(docname: str):
|
||||
frappe.has_permission("Bank Statement Import")
|
||||
frappe.has_permission("Bank Statement Import", throw=True)
|
||||
|
||||
return frappe.get_all(
|
||||
"Data Import Log",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
from rapidfuzz.utils import default_process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
processor=default_process,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool
|
||||
get_linked_payments,
|
||||
reconcile_vouchers,
|
||||
)
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -434,15 +437,13 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Wire Transfer"})
|
||||
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", gl_account)
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
@@ -195,19 +195,19 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-10 22:14:36.361509",
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
@@ -235,4 +235,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class Budget(Document):
|
||||
cost_center: DF.Link | None
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Data | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -136,9 +136,6 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import set_name_from_naming_options
|
||||
from frappe.utils import flt, fmt_money, now
|
||||
from frappe.utils import create_batch, flt, fmt_money, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -451,12 +451,15 @@ def rename_gle_sle_docs():
|
||||
def rename_temporarily_named_docs(doctype):
|
||||
"""Rename temporarily named docs using autoname options"""
|
||||
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
|
||||
for doc in docs_to_rename:
|
||||
oldname = doc.name
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
autoname = frappe.get_meta(doctype).autoname
|
||||
|
||||
for batch in create_batch(docs_to_rename, 100):
|
||||
for doc in batch:
|
||||
oldname = doc.name
|
||||
set_name_from_naming_options(autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -35,7 +35,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
finance_book: frm.doc.finance_book,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -168,8 +168,9 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
|
||||
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||
|
||||
if loyalty_amount > ref_doc.rounded_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
|
||||
total_amount = ref_doc.grand_total if ref_doc.is_rounded_total_disabled() else ref_doc.rounded_total
|
||||
if loyalty_amount > total_amount:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Total Amount."))
|
||||
|
||||
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
|
||||
ref_doc.loyalty_amount = loyalty_amount
|
||||
|
||||
@@ -3,8 +3,25 @@
|
||||
|
||||
import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('Mode of Payment')
|
||||
import frappe
|
||||
|
||||
|
||||
class TestModeofPayment(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):
|
||||
mode_of_payment.reload()
|
||||
if frappe.db.exists(
|
||||
"Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company}
|
||||
):
|
||||
frappe.db.set_value(
|
||||
"Mode of Payment Account",
|
||||
{"parent": mode_of_payment.mode_of_payment, "company": company},
|
||||
"default_account",
|
||||
account,
|
||||
)
|
||||
return
|
||||
|
||||
mode_of_payment.append("accounts", {"company": company, "default_account": account})
|
||||
mode_of_payment.save()
|
||||
|
||||
@@ -411,7 +411,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"advance_reconciliation_takes_effect_on",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -229,6 +228,7 @@
|
||||
"fieldname": "party_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Party Balance",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -786,18 +786,9 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Oldest Of Invoice Or Advance",
|
||||
"fetch_from": "company.reconciliation_takes_effect_on",
|
||||
"fieldname": "advance_reconciliation_takes_effect_on",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Advance Reconciliation Takes Effect On",
|
||||
"no_copy": 1,
|
||||
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
@@ -809,7 +800,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-24 16:18:19.920701",
|
||||
"modified": "2025-05-15 18:01:04.013025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -849,6 +840,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -1492,9 +1492,12 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
@@ -1504,7 +1507,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
@@ -1994,7 +1997,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
@@ -2045,7 +2048,7 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
@@ -2361,7 +2364,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
accounts = get_party_account(
|
||||
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
|
||||
)
|
||||
advance_account = accounts[1] if len(accounts) >= 1 else None
|
||||
advance_account = accounts[1] if len(accounts) > 1 else None
|
||||
|
||||
if party_account == advance_account:
|
||||
party_account = accounts[0]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Pegged Currencies", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:47:03.670913",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"pegged_currencies_item_section",
|
||||
"pegged_currency_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "pegged_currencies_item_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_currency_item",
|
||||
"fieldtype": "Table",
|
||||
"options": "Pegged Currency Details"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-02 11:46:31.936714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currencies",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencies(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
|
||||
|
||||
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
|
||||
|
||||
pegged_currency_item: DF.Table[PeggedCurrencies]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPeggedCurrencies(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:59:28.219277",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_currency",
|
||||
"pegged_against",
|
||||
"pegged_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "source_currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_exchange_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_against",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Pegged Against",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-17 14:11:16.521193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currency Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencyDetails(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
|
||||
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pegged_against: DF.Link | None
|
||||
pegged_exchange_rate: DF.Data | None
|
||||
source_currency: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -133,7 +133,12 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Account Closing Balance",
|
||||
)
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.cancel_gl_entries()
|
||||
|
||||
@@ -323,3 +323,15 @@ frappe.ui.form.on("POS Invoice", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Payment", {
|
||||
mode_of_payment: function (frm) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "set_account_for_mode_of_payment",
|
||||
callback: function (r) {
|
||||
refresh_field("payments");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -273,6 +273,8 @@ class POSInvoice(SalesInvoice):
|
||||
against_psi_doc.delete_loyalty_point_entry()
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import unittest
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -31,6 +34,8 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
cls.test_user, cls.pos_profile = init_user_and_profile()
|
||||
create_opening_entry(cls.pos_profile, cls.test_user)
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.session.user != "Administrator":
|
||||
@@ -233,12 +238,8 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos = create_pos_invoice(qty=10, do_not_save=True)
|
||||
|
||||
pos.set("payments", [])
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
@@ -276,9 +277,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
@@ -318,9 +317,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 2000, "default": 1})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
@@ -331,9 +328,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.set("payments", [])
|
||||
pos_return1.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return1.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
|
||||
pos_return1.paid_amount = -1000
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
@@ -350,9 +345,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.set("payments", [])
|
||||
pos_return2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return2.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
|
||||
pos_return2.paid_amount = -1000
|
||||
pos_return2.submit()
|
||||
|
||||
@@ -372,10 +365,8 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
pos.set("payments", [])
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50})
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60, "default": 1})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
@@ -393,7 +384,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
|
||||
{"mode_of_payment": "Cash", "amount": 9000},
|
||||
)
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
@@ -424,9 +415,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
@@ -445,9 +434,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos2.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
||||
)
|
||||
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||
|
||||
pos2.insert()
|
||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||
@@ -496,9 +483,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos2.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
||||
)
|
||||
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||
|
||||
pos2.insert()
|
||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||
@@ -561,9 +546,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
pos.get("items")[0].has_serial_no = 1
|
||||
pos.set("payments", [])
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
|
||||
pos = pos.save().submit()
|
||||
|
||||
# make a return
|
||||
@@ -609,7 +592,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
{"mode_of_payment": "Cash", "amount": 10000},
|
||||
)
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
@@ -641,7 +624,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
{"mode_of_payment": "Cash", "amount": 10000},
|
||||
)
|
||||
pos_inv.paid_amount = 10000
|
||||
pos_inv.submit()
|
||||
@@ -656,7 +639,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
|
||||
inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount},
|
||||
{"mode_of_payment": "Cash", "amount": 10000 - inv.loyalty_amount},
|
||||
)
|
||||
inv.paid_amount = 10000
|
||||
inv.submit()
|
||||
@@ -677,12 +660,12 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
@@ -703,7 +686,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
pos_inv.append(
|
||||
"taxes",
|
||||
{
|
||||
@@ -720,7 +703,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
||||
pos_inv2.additional_discount_percentage = 10
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540})
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 540})
|
||||
pos_inv2.append(
|
||||
"taxes",
|
||||
{
|
||||
@@ -758,7 +741,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
pos_inv.append(
|
||||
"taxes",
|
||||
{
|
||||
@@ -773,7 +756,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
|
||||
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400})
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 400})
|
||||
pos_inv2.append(
|
||||
"taxes",
|
||||
{
|
||||
@@ -818,7 +801,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
|
||||
{"mode_of_payment": "Cash", "amount": 4500},
|
||||
)
|
||||
pos_inv1.items[0].batch_no = batch_no
|
||||
pos_inv1.save()
|
||||
@@ -839,7 +822,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
pos_inv2.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
|
||||
{"mode_of_payment": "Cash", "amount": 3000},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
@@ -879,7 +862,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
|
||||
{"mode_of_payment": "Cash", "amount": 300},
|
||||
)
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
@@ -258,6 +258,7 @@ class POSInvoiceMergeLog(Document):
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
tax.row_id = None
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
|
||||
@@ -60,6 +60,20 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"posting_date",
|
||||
"company",
|
||||
"account",
|
||||
"group_by",
|
||||
"categorize_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
@@ -174,14 +174,6 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"default": "Group by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "group_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group By",
|
||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "currency",
|
||||
@@ -397,10 +389,18 @@
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
},
|
||||
{
|
||||
"default": "Categorize by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "categorize_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-12-11 12:11:13.543134",
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
@@ -436,4 +436,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
||||
based_on_payment_terms: DF.Check
|
||||
body: DF.TextEditor | None
|
||||
categorize_by: DF.Literal["", "Categorize by Voucher", "Categorize by Voucher (Consolidated)"]
|
||||
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
||||
collection_name: DF.DynamicLink | None
|
||||
company: DF.Link
|
||||
@@ -56,7 +57,6 @@ class ProcessStatementOfAccounts(Document):
|
||||
finance_book: DF.Link | None
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
@@ -129,8 +129,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
|
||||
tax_id = frappe.get_doc("Customer", entry.customer).tax_id
|
||||
presentation_currency = (
|
||||
get_party_account_currency("Customer", entry.customer, doc.company)
|
||||
or doc.currency
|
||||
doc.currency
|
||||
or get_party_account_currency("Customer", entry.customer, doc.company)
|
||||
or get_company_currency(doc.company)
|
||||
)
|
||||
|
||||
@@ -204,7 +204,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"categorize_by": doc.categorize_by,
|
||||
"currency": doc.currency,
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
{% if(data[i]["party"]) %}
|
||||
<td>{{ (data[i]["posting_date"]) }}</td>
|
||||
<td>{{ frappe.format((data[i]["posting_date"]), 'Date') }}</td>
|
||||
<td style="text-align: right">{{ data[i]["age"] }}</td>
|
||||
<td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
|
||||
@@ -97,6 +97,7 @@ def create_process_soa(**args):
|
||||
company=args.company or "_Test Company",
|
||||
customers=args.customers or [{"customer": "_Test Customer"}],
|
||||
enable_auto_email=1 if args.enable_auto_email else 0,
|
||||
currency=args.currency or "",
|
||||
frequency=args.frequency or "Weekly",
|
||||
report=args.report or "General Ledger",
|
||||
from_date=args.from_date or getdate(today()),
|
||||
|
||||
@@ -425,6 +425,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.set_value("is_paid", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
}
|
||||
} else {
|
||||
this.frm.set_value("paid_amount", 0);
|
||||
}
|
||||
this.calculate_outstanding_amount();
|
||||
this.frm.refresh_fields();
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
@@ -55,6 +55,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_purchase_invoice_qty(self):
|
||||
pi = make_purchase_invoice(qty=0, do_not_save=True)
|
||||
with self.assertRaises(InvalidQtyError):
|
||||
pi.save()
|
||||
|
||||
# No error with qty=1
|
||||
pi.items[0].qty = 1
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].qty, 1)
|
||||
|
||||
def test_purchase_invoice_received_qty(self):
|
||||
"""
|
||||
1. Test if received qty is validated against accepted + rejected
|
||||
@@ -1695,6 +1705,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# Configure Buying Settings to allow rate change
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
# Configure Accounts Settings to allow 300% over billing
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300)
|
||||
|
||||
# Create PR: rate = 1000, qty = 5
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
|
||||
@@ -2756,6 +2769,54 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
def test_pr_pi_over_billing(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
|
||||
# Configure Buying Settings to allow rate change
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
pr = make_purchase_receipt(qty=10, rate=10)
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
|
||||
pi.items[0].rate = 12
|
||||
|
||||
# Test 1 - This will fail because over billing is not allowed
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
# Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked
|
||||
pi.submit()
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20)
|
||||
pi.cancel()
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
pi.items[0].rate = 12
|
||||
|
||||
# Test 3 - This will now submit because over billing is allowed upto 20%
|
||||
pi.submit()
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
pi.items[0].rate = 13
|
||||
|
||||
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
|
||||
pi = make_purchase_invoice(do_not_save=True)
|
||||
discount_amount = 7
|
||||
pi.discount_amount = discount_amount
|
||||
pi.save()
|
||||
self.assertEqual(pi.additional_discount_percentage, None)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(today(), -1)
|
||||
pi.save()
|
||||
self.assertEqual(pi.discount_amount, discount_amount)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
@@ -2865,7 +2926,7 @@ def make_purchase_invoice(**args):
|
||||
bundle_id = None
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
batches = {}
|
||||
qty = args.qty or 5
|
||||
qty = args.qty if args.qty is not None else 5
|
||||
item_code = args.item or args.item_code or "_Test Item"
|
||||
if args.get("batch_no"):
|
||||
batches = frappe._dict({args.batch_no: qty})
|
||||
@@ -2894,7 +2955,7 @@ def make_purchase_invoice(**args):
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"item_name": args.item_name,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 5,
|
||||
"qty": args.qty if args.qty is not None else 5,
|
||||
"received_qty": args.received_qty or 0,
|
||||
"rejected_qty": args.rejected_qty or 0,
|
||||
"rate": args.rate or 50,
|
||||
|
||||
@@ -1084,6 +1084,18 @@ frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Payment", {
|
||||
mode_of_payment: function (frm) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "set_account_for_mode_of_payment",
|
||||
callback: function (r) {
|
||||
refresh_field("payments");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
|
||||
frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",
|
||||
|
||||
@@ -751,10 +751,10 @@ class SalesInvoice(SellingController):
|
||||
self.paid_amount = paid_amount
|
||||
self.base_paid_amount = base_paid_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_account_for_mode_of_payment(self):
|
||||
for payment in self.payments:
|
||||
if not payment.account:
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||
|
||||
def validate_time_sheets_are_submitted(self):
|
||||
for data in self.timesheets:
|
||||
@@ -2286,6 +2286,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
set_purchase_references(target)
|
||||
|
||||
def update_details(source_doc, target_doc, source_parent):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": link_doctype,
|
||||
"link_name": link_name,
|
||||
},
|
||||
"parent",
|
||||
)
|
||||
|
||||
target_doc.inter_company_invoice_reference = source_doc.name
|
||||
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
|
||||
currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
|
||||
@@ -2296,16 +2308,34 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_doc.buying_price_list = source_doc.selling_price_list
|
||||
|
||||
# Invert Addresses
|
||||
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
|
||||
update_address(
|
||||
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
|
||||
)
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
if source_doc.company_address and _validate_address_link(
|
||||
source_doc.company_address, "Supplier", details.get("party")
|
||||
):
|
||||
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
|
||||
if source_doc.dispatch_address_name and _validate_address_link(
|
||||
source_doc.dispatch_address_name, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc,
|
||||
"dispatch_address",
|
||||
"dispatch_address_display",
|
||||
source_doc.dispatch_address_name,
|
||||
)
|
||||
if source_doc.shipping_address_name and _validate_address_link(
|
||||
source_doc.shipping_address_name, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc,
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
source_doc.shipping_address_name,
|
||||
)
|
||||
if source_doc.customer_address and _validate_address_link(
|
||||
source_doc.customer_address, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
@@ -2326,13 +2356,22 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_doc.customer = details.get("party")
|
||||
target_doc.selling_price_list = source_doc.buying_price_list
|
||||
|
||||
update_address(
|
||||
target_doc, "company_address", "company_address_display", source_doc.supplier_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
|
||||
)
|
||||
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
|
||||
if source_doc.supplier_address and _validate_address_link(
|
||||
source_doc.supplier_address, "Company", details.get("company")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "company_address", "company_address_display", source_doc.supplier_address
|
||||
)
|
||||
if source_doc.shipping_address and _validate_address_link(
|
||||
source_doc.shipping_address, "Customer", details.get("party")
|
||||
):
|
||||
update_address(
|
||||
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
|
||||
)
|
||||
if source_doc.shipping_address and _validate_address_link(
|
||||
source_doc.shipping_address, "Customer", details.get("party")
|
||||
):
|
||||
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
@@ -12,6 +12,9 @@ from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||
@@ -24,7 +27,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
|
||||
@@ -54,8 +57,33 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||
create_internal_parties()
|
||||
setup_accounts()
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
|
||||
set_default_account_for_mode_of_payment(
|
||||
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
|
||||
)
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"maintain_same_internal_transaction_rate": 1, "maintain_same_rate_action": "Stop"},
|
||||
)
|
||||
def test_invalid_rate_without_override(self):
|
||||
from frappe import ValidationError
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100
|
||||
)
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.items[0].rate = 120
|
||||
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
self.assertIn("Rate must be same", str(e.exception))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
@@ -74,6 +102,16 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
def tearDownClass(self):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
|
||||
def test_sales_invoice_qty(self):
|
||||
si = create_sales_invoice(qty=0, do_not_save=True)
|
||||
with self.assertRaises(InvalidQtyError):
|
||||
si.save()
|
||||
|
||||
# No error with qty=1
|
||||
si.items[0].qty = 1
|
||||
si.save()
|
||||
self.assertEqual(si.items[0].qty, 1)
|
||||
|
||||
def test_timestamp_change(self):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
w.docstatus = 0
|
||||
@@ -964,10 +1002,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos.is_pos = 1
|
||||
pos.update_stock = 1
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 50})
|
||||
|
||||
taxes = get_taxes_and_charges()
|
||||
pos.taxes = []
|
||||
@@ -996,10 +1032,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
@@ -1042,10 +1076,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos.is_pos = 1
|
||||
pos.update_stock = 1
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
|
||||
|
||||
pos.write_off_outstanding_amount_automatically = 1
|
||||
pos.insert()
|
||||
@@ -1085,10 +1117,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos.is_pos = 1
|
||||
pos.update_stock = 1
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 40})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 40})
|
||||
|
||||
pos.write_off_outstanding_amount_automatically = 1
|
||||
pos.insert()
|
||||
@@ -1102,7 +1132,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
pos = create_sales_invoice(do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 100})
|
||||
pos.save().submit()
|
||||
self.assertEqual(pos.outstanding_amount, 0.0)
|
||||
self.assertEqual(pos.status, "Paid")
|
||||
@@ -1173,10 +1203,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for tax in taxes:
|
||||
pos.append("taxes", tax)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
@@ -2573,6 +2601,62 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": link_doctype,
|
||||
"link_name": link_name,
|
||||
},
|
||||
"parent",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.submit()
|
||||
|
||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
target_doc.items[0].update(
|
||||
{
|
||||
"expense_account": "Cost of Goods Sold - _TC1",
|
||||
"cost_center": "Main - _TC1",
|
||||
"warehouse": "Stores - _TC1",
|
||||
}
|
||||
)
|
||||
target_doc.save()
|
||||
|
||||
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
|
||||
for details in [
|
||||
("supplier_address", "Supplier", target_doc.supplier),
|
||||
("dispatch_address", "Company", target_doc.company),
|
||||
("shipping_address", "Company", target_doc.company),
|
||||
("billing_address", "Company", target_doc.company),
|
||||
]:
|
||||
if address := target_doc.get(details[0]):
|
||||
self.assertEqual(address, _validate_address_link(address, details[1], details[2]))
|
||||
else:
|
||||
for details in [
|
||||
("company_address", "Company", target_doc.company),
|
||||
("shipping_address_name", "Customer", target_doc.customer),
|
||||
("customer_address", "Customer", target_doc.customer),
|
||||
]:
|
||||
if address := target_doc.get(details[0]):
|
||||
self.assertEqual(address, _validate_address_link(address, details[1], details[2]))
|
||||
|
||||
def test_inter_company_transaction(self):
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
@@ -3904,10 +3988,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
@@ -4278,7 +4360,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.debit_to = "_Test Receivable USD - _TC"
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 20.35})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
@@ -4379,11 +4461,12 @@ def create_sales_invoice(**args):
|
||||
si.conversion_rate = args.conversion_rate or 1
|
||||
si.naming_series = args.naming_series or "T-SINV-"
|
||||
si.cost_center = args.parent_cost_center
|
||||
si.is_internal_customer = args.is_internal_customer or 0
|
||||
|
||||
bundle_id = None
|
||||
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
|
||||
batches = {}
|
||||
qty = args.qty or 1
|
||||
qty = args.qty if args.qty is not None else 1
|
||||
item_code = args.item or args.item_code or "_Test Item"
|
||||
if args.get("batch_no"):
|
||||
batches = frappe._dict({args.batch_no: qty})
|
||||
@@ -4415,7 +4498,7 @@ def create_sales_invoice(**args):
|
||||
"description": args.description or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"target_warehouse": args.target_warehouse,
|
||||
"qty": args.qty or 1,
|
||||
"qty": args.qty if args.qty is not None else 1,
|
||||
"uom": args.uom or "Nos",
|
||||
"stock_uom": args.uom or "Nos",
|
||||
"rate": args.rate if args.get("rate") is not None else 100,
|
||||
@@ -4581,6 +4664,12 @@ def create_internal_parties():
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier 3",
|
||||
represents_company="_Test Company",
|
||||
allowed_to_interact_with="_Test Company",
|
||||
)
|
||||
|
||||
|
||||
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
|
||||
@@ -671,6 +671,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
|
||||
@@ -288,17 +288,18 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
pe1 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
pe1.paid_from = "Debtors - _TC"
|
||||
pe1.paid_to = "Cash - _TC"
|
||||
pe1.submit()
|
||||
vouchers.append(pe1)
|
||||
|
||||
# create invoice
|
||||
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
|
||||
@@ -320,6 +321,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
# make another invoice
|
||||
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
|
||||
# TDS should be calculated
|
||||
|
||||
# this payment should not be considered for TCS calculation as it is outside of fiscal year
|
||||
pe2 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
|
||||
)
|
||||
pe2.paid_from = "Debtors - _TC"
|
||||
pe2.paid_to = "Cash - _TC"
|
||||
pe2.posting_date = add_days(fiscal_year[1], -10)
|
||||
pe2.submit()
|
||||
vouchers.append(pe2)
|
||||
|
||||
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
|
||||
si2.submit()
|
||||
vouchers.append(si2)
|
||||
|
||||
@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
||||
Will first search in party (Customer / Supplier) record, if not found,
|
||||
will search in group (Customer Group / Supplier Group),
|
||||
finally will return default."""
|
||||
if not party_type:
|
||||
frappe.throw(_("Party Type is mandatory"))
|
||||
if not company:
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
@@ -460,6 +462,12 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
||||
if (account and account_currency != existing_gle_currency) or not account:
|
||||
account = get_party_gle_account(party_type, party, company)
|
||||
|
||||
# get default account on the basis of party type
|
||||
if not account:
|
||||
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
|
||||
default_account_name = "default_" + account_type.lower() + "_account"
|
||||
account = frappe.get_cached_value("Company", company, default_account_name)
|
||||
|
||||
if include_advance and party_type in ["Customer", "Supplier", "Student"]:
|
||||
advance_account = get_party_advance_account(party_type, party, company)
|
||||
if advance_account:
|
||||
@@ -657,34 +665,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
return due_date
|
||||
|
||||
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
else:
|
||||
if not template_name:
|
||||
return
|
||||
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
|
||||
|
||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
|
||||
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
|
||||
|
||||
msgprint(
|
||||
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
|
||||
party_type, date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.throw(_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -931,12 +939,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
fields=["name", "is_shipping_address"],
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
|
||||
return shipping_addresses[0].name
|
||||
if len(shipping_addresses) == 1:
|
||||
return shipping_addresses[0].name
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
|
||||
@@ -60,6 +60,13 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
options: "Posting Date\nDue Date\nSupplier Invoice Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
<td>
|
||||
{% if(!(filters.party)) { %}
|
||||
{% if(!filters.party?.length) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
@@ -258,7 +258,7 @@
|
||||
{% if(data[i]["party"]|| " ") { %}
|
||||
{% if(!data[i]["is_total_row"]) { %}
|
||||
<td>
|
||||
{% if(!(filters.party)) { %}
|
||||
{% if(!filters.party?.length) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
|
||||
@@ -89,6 +89,13 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -47,13 +48,20 @@ class ReceivablePayableReport:
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||
self.age_as_on = (
|
||||
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
|
||||
getdate(nowdate())
|
||||
if "calculate_ageing_with" not in self.filters
|
||||
or self.filters.calculate_ageing_with == "Today Date"
|
||||
else self.filters.report_date
|
||||
)
|
||||
|
||||
if not self.filters.range:
|
||||
self.filters.range = "30, 60, 90, 120"
|
||||
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
||||
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
||||
self.ple_fetch_method = (
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -90,13 +98,7 @@ class ReceivablePayableReport:
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
# Get invoice details like bill_no, due_date etc for all invoices
|
||||
self.get_invoice_details()
|
||||
@@ -105,17 +107,51 @@ class ReceivablePayableReport:
|
||||
self.get_future_payments()
|
||||
|
||||
# Get return entries
|
||||
self.get_return_entries()
|
||||
if not self.filters.party_type or self.filters.party_type in ["Customer", "Supplier"]:
|
||||
self.get_return_entries()
|
||||
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.prepare_ple_query()
|
||||
self.data = []
|
||||
self.voucher_balance = OrderedDict()
|
||||
|
||||
if self.ple_fetch_method == "Buffered Cursor":
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
query, param = self.ple_query
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
|
||||
self.build_data()
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
@@ -136,26 +172,22 @@ class ReceivablePayableReport:
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
def init_voucher_balance(self, ple):
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
@@ -419,16 +451,14 @@ class ReceivablePayableReport:
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.account_type == "Receivable":
|
||||
# nosemgrep
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
from `tabSales Invoice`
|
||||
where posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
si_list = frappe.get_list(
|
||||
"Sales Invoice",
|
||||
filters={
|
||||
"posting_date": ("<=", self.filters.report_date),
|
||||
"company": self.filters.company,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "due_date", "po_no"],
|
||||
)
|
||||
for d in si_list:
|
||||
self.invoice_details.setdefault(d.name, d)
|
||||
@@ -451,33 +481,29 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.account_type == "Payable":
|
||||
# nosemgrep
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabPurchase Invoice`
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
):
|
||||
invoices = frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
filters={
|
||||
"posting_date": ("<=", self.filters.report_date),
|
||||
"company": self.filters.company,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "due_date", "bill_no", "bill_date"],
|
||||
)
|
||||
|
||||
for pi in invoices:
|
||||
self.invoice_details.setdefault(pi.name, pi)
|
||||
|
||||
# Invoices booked via Journal Entries
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabJournal Entry`
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
journal_entries = frappe.get_list(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"posting_date": ("<=", self.filters.report_date),
|
||||
"company": self.filters.company,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "due_date", "bill_no", "bill_date"],
|
||||
)
|
||||
|
||||
for je in journal_entries:
|
||||
@@ -759,7 +785,7 @@ class ReceivablePayableReport:
|
||||
self.get_ageing_data(entry_date, row)
|
||||
|
||||
# ageing buckets should not have amounts if due date is not reached
|
||||
if getdate(entry_date) > getdate(self.filters.report_date):
|
||||
if getdate(entry_date) > getdate(self.age_as_on):
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
|
||||
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
|
||||
@@ -778,7 +804,7 @@ class ReceivablePayableReport:
|
||||
)
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
def prepare_ple_query(self):
|
||||
# get all the GL entries filtered by the given filters
|
||||
|
||||
self.prepare_conditions()
|
||||
@@ -826,12 +852,18 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
query, param = query.walk()
|
||||
|
||||
self.ple_entries = query.run(as_dict=True)
|
||||
match_conditions = build_match_conditions("Payment Ledger Entry")
|
||||
if match_conditions:
|
||||
query += " AND " + match_conditions
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
|
||||
else:
|
||||
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
|
||||
|
||||
self.ple_query = (query, param)
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.query_reports["Calculated Discount Mismatch"] = {
|
||||
// filters: [
|
||||
// {
|
||||
// "fieldname": "my_filter",
|
||||
// "label": __("My Filter"),
|
||||
// "fieldtype": "Data",
|
||||
// "reqd": 1,
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2025-06-06 17:09:50.681090",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "",
|
||||
"letterhead": null,
|
||||
"modified": "2025-06-06 18:09:18.221911",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Calculated Discount Mismatch",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Version",
|
||||
"report_name": "Calculated Discount Mismatch",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"role": "Administrator"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Order, Tuple
|
||||
from frappe.utils.formatters import format_value
|
||||
|
||||
AFFECTED_DOCTYPES = frozenset(
|
||||
(
|
||||
"POS Invoice",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Purchase Order",
|
||||
"Supplier Quotation",
|
||||
"Quotation",
|
||||
"Sales Order",
|
||||
"Delivery Note",
|
||||
"Purchase Receipt",
|
||||
)
|
||||
)
|
||||
LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30"
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = get_data()
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "doctype",
|
||||
"label": _("Transaction Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "docname",
|
||||
"label": _("Transaction Name"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "doctype",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_discount_percentage",
|
||||
"label": _("Discount Percentage in Transaction"),
|
||||
"fieldtype": "Percent",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_discount_amount",
|
||||
"label": _("Discount Amount in Transaction"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "suspected_discount_amount",
|
||||
"label": _("Suspected Discount Amount"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 180,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data():
|
||||
transactions_with_discount_percentage = {}
|
||||
|
||||
for doctype in AFFECTED_DOCTYPES:
|
||||
transactions = get_transactions_with_discount_percentage(doctype)
|
||||
|
||||
for transaction in transactions:
|
||||
transactions_with_discount_percentage[(doctype, transaction.name)] = transaction
|
||||
|
||||
if not transactions_with_discount_percentage:
|
||||
return []
|
||||
|
||||
VERSION = frappe.qb.DocType("Version")
|
||||
|
||||
versions = (
|
||||
frappe.qb.from_(VERSION)
|
||||
.select(VERSION.ref_doctype, VERSION.docname, VERSION.data)
|
||||
.where(VERSION.creation > LAST_MODIFIED_DATE_THRESHOLD)
|
||||
.where(Tuple(VERSION.ref_doctype, VERSION.docname).isin(list(transactions_with_discount_percentage)))
|
||||
.where(
|
||||
VERSION.data.like('%"discount\\_amount"%')
|
||||
| VERSION.data.like('%"additional\\_discount\\_percentage"%')
|
||||
)
|
||||
.orderby(VERSION.creation, order=Order.desc)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if not versions:
|
||||
return []
|
||||
|
||||
version_map = {}
|
||||
for version in versions:
|
||||
key = (version.ref_doctype, version.docname)
|
||||
if key not in version_map:
|
||||
version_map[key] = []
|
||||
|
||||
version_map[key].append(version.data)
|
||||
|
||||
data = []
|
||||
discount_amount_field_map = {
|
||||
doctype: frappe.get_meta(doctype).get_field("discount_amount") for doctype in AFFECTED_DOCTYPES
|
||||
}
|
||||
for doc, versions in version_map.items():
|
||||
for version_data in versions:
|
||||
if '"additional_discount_percentage"' in version_data:
|
||||
# don't consider doc if additional_discount_percentage is changed in newest version
|
||||
break
|
||||
|
||||
version_data = json.loads(version_data)
|
||||
changed_values = version_data.get("changed")
|
||||
if not changed_values:
|
||||
continue
|
||||
|
||||
discount_values = next((row for row in changed_values if row[0] == "discount_amount"), None)
|
||||
if not discount_values:
|
||||
continue
|
||||
|
||||
old = discount_values[1]
|
||||
new = discount_values[2]
|
||||
doctype = doc[0]
|
||||
doc_values = transactions_with_discount_percentage.get(doc)
|
||||
formatted_discount_amount = format_value(
|
||||
doc_values.discount_amount,
|
||||
df=discount_amount_field_map[doctype],
|
||||
currency=doc_values.currency,
|
||||
)
|
||||
|
||||
if new != formatted_discount_amount:
|
||||
# if the discount amount in the version is not equal to the current value, skip
|
||||
break
|
||||
|
||||
data.append(
|
||||
{
|
||||
"doctype": doctype,
|
||||
"docname": doc_values.name,
|
||||
"actual_discount_percentage": doc_values.additional_discount_percentage,
|
||||
"actual_discount_amount": new,
|
||||
"suspected_discount_amount": old,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_transactions_with_discount_percentage(doctype):
|
||||
transactions = frappe.get_all(
|
||||
doctype,
|
||||
fields=[
|
||||
"name",
|
||||
"currency",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
],
|
||||
filters={
|
||||
"docstatus": ["<", 2],
|
||||
"additional_discount_percentage": [">", 0],
|
||||
"discount_amount": ["!=", 0],
|
||||
"modified": [">", LAST_MODIFIED_DATE_THRESHOLD],
|
||||
},
|
||||
)
|
||||
|
||||
return transactions
|
||||
@@ -75,7 +75,11 @@ def execute(filters=None):
|
||||
# add first net income in operations section
|
||||
if net_profit_loss:
|
||||
net_profit_loss.update(
|
||||
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]}
|
||||
{
|
||||
"indent": 1,
|
||||
"parent_section": cash_flow_sections[0]["section_header"],
|
||||
"section": net_profit_loss["account"],
|
||||
}
|
||||
)
|
||||
data.append(net_profit_loss)
|
||||
section_data.append(net_profit_loss)
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.query_builder import Criterion, Tuple
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from pypika.terms import LiteralValue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -15,7 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
|
||||
TREE_DOCTYPES = frozenset(
|
||||
["Customer Group", "Terrirtory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
|
||||
["Customer Group", "Territory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
|
||||
)
|
||||
|
||||
|
||||
@@ -77,13 +78,12 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(party_type)
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
query = query.where(LiteralValue(match_conditions))
|
||||
|
||||
party_details = frappe.db.sql(query, params, as_dict=True)
|
||||
party_details = query.run(as_dict=True)
|
||||
|
||||
for row in party_details:
|
||||
self.parties.append(row.party)
|
||||
@@ -144,10 +144,10 @@ class PartyLedgerSummaryReport:
|
||||
if self.party_naming_by == "Naming Series":
|
||||
columns.append(
|
||||
{
|
||||
"label": _(self.filters.party_type + "Name"),
|
||||
"label": _(self.filters.party_type + " Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "party_name",
|
||||
"width": 110,
|
||||
"width": 150,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -252,12 +252,13 @@ class PartyLedgerSummaryReport:
|
||||
self.party_data = frappe._dict({})
|
||||
for gle in self.gl_entries:
|
||||
party_details = self.party_details.get(gle.party)
|
||||
party_name = party_details.get(f"{scrub(self.filters.party_type)}_name", "")
|
||||
self.party_data.setdefault(
|
||||
gle.party,
|
||||
frappe._dict(
|
||||
{
|
||||
**party_details,
|
||||
"party_name": gle.party,
|
||||
"party_name": party_name,
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 0,
|
||||
"paid_amount": 0,
|
||||
@@ -404,7 +405,9 @@ class PartyLedgerSummaryReport:
|
||||
gl = qb.DocType("GL Entry")
|
||||
query = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_type, gl.voucher_no)
|
||||
.select(
|
||||
gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
|
||||
)
|
||||
.where(
|
||||
(gl.docstatus < 2)
|
||||
& (gl.is_cancelled == 0)
|
||||
@@ -455,9 +458,16 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
|
||||
def get_children(doctype, value):
|
||||
children = get_descendants_of(doctype, value)
|
||||
if not isinstance(value, list):
|
||||
value = [d.strip() for d in value.strip().split(",") if d]
|
||||
|
||||
return [value, *children]
|
||||
all_children = []
|
||||
|
||||
for d in value:
|
||||
all_children += get_descendants_of(doctype, value)
|
||||
all_children.append(d)
|
||||
|
||||
return list(set(all_children))
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
{% for(let j=0, k=data.length; j<k; j++) { %}
|
||||
{%
|
||||
var row = data[j];
|
||||
var row_class = data[j].parent_account ? "" : "financial-statements-important";
|
||||
row_class += data[j].account_name ? "" : " financial-statements-blank-row";
|
||||
var row_class = data[j].parent_account || data[j].parent_section ? "" : "financial-statements-important";
|
||||
row_class += data[j].account_name || data[j].section ? "" : " financial-statements-blank-row";
|
||||
%}
|
||||
<tr class="{%= row_class %}">
|
||||
<td>
|
||||
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name %}</span>
|
||||
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name || row.section %}</span>
|
||||
</td>
|
||||
{% for(let i=1, l=report_columns.length; i<l; i++) { %}
|
||||
<td class="text-right">
|
||||
|
||||
@@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
"",
|
||||
{
|
||||
label: __("Group by Voucher"),
|
||||
value: "Group by Voucher",
|
||||
label: __("Categorize by Voucher"),
|
||||
value: "Categorize by Voucher",
|
||||
},
|
||||
{
|
||||
label: __("Group by Voucher (Consolidated)"),
|
||||
value: "Group by Voucher (Consolidated)",
|
||||
label: __("Categorize by Voucher (Consolidated)"),
|
||||
value: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
label: __("Group by Account"),
|
||||
value: "Group by Account",
|
||||
label: __("Categorize by Account"),
|
||||
value: "Categorize by Account",
|
||||
},
|
||||
{
|
||||
label: __("Group by Party"),
|
||||
value: "Group by Party",
|
||||
label: __("Categorize by Party"),
|
||||
value: "Categorize by Party",
|
||||
},
|
||||
],
|
||||
default: "Group by Voucher (Consolidated)",
|
||||
default: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
fieldname: "tax_id",
|
||||
|
||||
@@ -63,13 +63,17 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
||||
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||
filters["categorize_by"] = filters["group_by"]
|
||||
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||
|
||||
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
|
||||
filters.account = frappe.parse_json(filters.get("account"))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
||||
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@@ -163,9 +167,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if filters.get("include_dimensions"):
|
||||
order_by_statement = "order by posting_date, creation"
|
||||
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
if filters.get("categorize_by") == "Categorize by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
@@ -190,7 +194,8 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
||||
cost_center, project, {transaction_currency_fields}
|
||||
against_voucher_type, against_voucher, account_currency,
|
||||
against, is_opening, creation {select_fields}
|
||||
against, is_opening, creation {select_fields},
|
||||
transaction_currency
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s {get_conditions(filters)}
|
||||
{order_by_statement}
|
||||
@@ -260,7 +265,7 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
if filters.get("party_type"):
|
||||
@@ -272,7 +277,7 @@ def get_conditions(filters):
|
||||
if not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
@@ -374,26 +379,26 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
for _acc, acc_dict in gle_map.items():
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
|
||||
# totals
|
||||
if filters.get("group_by") or not filters.voucher_no:
|
||||
if filters.get("categorize_by") or not filters.voucher_no:
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.closing)
|
||||
|
||||
@@ -430,9 +435,9 @@ def get_totals_dict():
|
||||
|
||||
|
||||
def group_by_field(group_by):
|
||||
if group_by == "Group by Party":
|
||||
if group_by == "Categorize by Party":
|
||||
return "party"
|
||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
||||
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
|
||||
return "account"
|
||||
else:
|
||||
return "voucher_no"
|
||||
@@ -440,7 +445,7 @@ def group_by_field(group_by):
|
||||
|
||||
def initialize_gle_map(gl_entries, filters, totals_dict):
|
||||
gle_map = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
|
||||
for gle in gl_entries:
|
||||
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
|
||||
@@ -450,8 +455,8 @@ def initialize_gle_map(gl_entries, filters, totals_dict):
|
||||
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
|
||||
entries = []
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
|
||||
|
||||
if filters.get("show_net_values_in_party_account"):
|
||||
account_type_map = get_account_type_map(filters.get("company"))
|
||||
@@ -559,17 +564,20 @@ def get_account_type_map(company):
|
||||
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
balance = get_balance(d, balance, "debit", "credit")
|
||||
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
|
||||
d["presentation_currency"] = filters.presentation_currency
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -595,11 +603,8 @@ def get_columns(filters):
|
||||
if filters.get("presentation_currency"):
|
||||
currency = filters["presentation_currency"]
|
||||
else:
|
||||
if filters.get("company"):
|
||||
currency = get_company_currency(filters["company"])
|
||||
else:
|
||||
company = get_default_company()
|
||||
currency = get_company_currency(company)
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
columns = [
|
||||
{
|
||||
@@ -620,19 +625,22 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Debit ({0})").format(currency),
|
||||
"fieldname": "debit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Credit ({0})").format(currency),
|
||||
"fieldname": "credit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Balance ({0})").format(currency),
|
||||
"fieldname": "balance",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
@@ -155,7 +158,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -168,6 +171,90 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||
def test_debit_in_exchange_gain_loss_account(self):
|
||||
company = "_Test Company"
|
||||
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||
if not exchange_gain_loss_account:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
account_name = "_Test Receivable USD - _TC"
|
||||
customer_name = "_Test Customer USD"
|
||||
|
||||
sales_invoice = create_sales_invoice(
|
||||
company=company,
|
||||
customer=customer_name,
|
||||
currency="USD",
|
||||
debit_to=account_name,
|
||||
conversion_rate=85,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
payment_entry = create_payment_entry(
|
||||
company=company,
|
||||
party_type="Customer",
|
||||
party=customer_name,
|
||||
payment_type="Receive",
|
||||
paid_from=account_name,
|
||||
paid_from_account_currency="USD",
|
||||
paid_to="Cash - _TC",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=10,
|
||||
do_not_submit=True,
|
||||
)
|
||||
payment_entry.base_paid_amount = 800
|
||||
payment_entry.received_amount = 800
|
||||
payment_entry.currency = "USD"
|
||||
payment_entry.source_exchange_rate = 80
|
||||
payment_entry.append(
|
||||
"references",
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": sales_invoice.name,
|
||||
"total_amount": 10,
|
||||
"outstanding_amount": 10,
|
||||
"exchange_rate": 85,
|
||||
"allocated_amount": 10,
|
||||
"exchange_gain_loss": -50,
|
||||
}
|
||||
),
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
journal_entry = frappe.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||
)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"include_dimensions": 1,
|
||||
"include_default_book_entries": 1,
|
||||
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
entry = data[1]
|
||||
self.assertEqual(entry["debit"], 50)
|
||||
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||
|
||||
payment_entry.cancel()
|
||||
payment_entry.delete()
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
sales_invoice.delete()
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
@@ -246,7 +333,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": True,
|
||||
}
|
||||
)
|
||||
@@ -261,7 +348,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": False,
|
||||
}
|
||||
)
|
||||
@@ -308,7 +395,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
@@ -325,7 +412,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
@@ -375,7 +376,12 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
query = query.where(sii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(sii.item_group == filters.get("item_group"))
|
||||
if frappe.db.get_value("Item Group", filters.get("item_group"), "is_group"):
|
||||
item_groups = get_descendants_of("Item Group", filters.get("item_group"))
|
||||
item_groups.append(filters.get("item_group"))
|
||||
query = query.where(sii.item_group.isin(item_groups))
|
||||
else:
|
||||
query = query.where(sii.item_group == filters.get("item_group"))
|
||||
|
||||
if filters.get("income_account"):
|
||||
query = query.where(
|
||||
|
||||
@@ -35,7 +35,6 @@ def execute(filters=None):
|
||||
filters=filters,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
ignore_closing_entries=True,
|
||||
ignore_accumulated_values_for_fy=True,
|
||||
)
|
||||
|
||||
expense = get_data(
|
||||
@@ -46,7 +45,6 @@ def execute(filters=None):
|
||||
filters=filters,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
ignore_closing_entries=True,
|
||||
ignore_accumulated_values_for_fy=True,
|
||||
)
|
||||
|
||||
net_profit_loss = get_net_profit_loss(
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.desk.query_report import export_query
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
@@ -57,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
|
||||
period_end_date=fy.year_end_date,
|
||||
filter_based_on="Fiscal Year",
|
||||
periodicity="Monthly",
|
||||
accumulated_vallues=True,
|
||||
accumulated_values=False,
|
||||
)
|
||||
|
||||
def test_profit_and_loss_output_and_summary(self):
|
||||
@@ -90,3 +91,82 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
|
||||
with self.subTest(current_period_key=current_period_key):
|
||||
self.assertEqual(acc[current_period_key], 150)
|
||||
self.assertEqual(acc["total"], 150)
|
||||
|
||||
def test_p_and_l_export(self):
|
||||
self.create_sales_invoice(qty=1, rate=150)
|
||||
|
||||
filters = self.get_report_filters()
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
{
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"file_format_type": "CSV",
|
||||
"filters": filters,
|
||||
"visible_idx": [0, 1, 2, 3, 4, 5, 6],
|
||||
}
|
||||
)
|
||||
export_query()
|
||||
contents = frappe.response["filecontent"].decode()
|
||||
sales_account = frappe.db.get_value("Company", self.company, "default_income_account")
|
||||
|
||||
self.assertIn(sales_account, contents)
|
||||
|
||||
def test_accumulate_filter(self):
|
||||
# ensure 2 fiscal years
|
||||
cur_fy = self.get_fiscal_year()
|
||||
find_for = add_days(cur_fy.year_start_date, -1)
|
||||
_x = frappe.db.get_all(
|
||||
"Fiscal Year",
|
||||
filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)},
|
||||
)[0]
|
||||
prev_fy = frappe.get_doc("Fiscal Year", _x.name)
|
||||
prev_fy.append("companies", {"company": self.company})
|
||||
prev_fy.save()
|
||||
|
||||
# make SI on both of them
|
||||
prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True)
|
||||
prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1)
|
||||
prev_fy_si.save().submit()
|
||||
income_acc = prev_fy_si.items[0].income_account
|
||||
|
||||
self.create_sales_invoice(qty=1, rate=120)
|
||||
|
||||
# Unaccumualted
|
||||
filters = frappe._dict(
|
||||
company=self.company,
|
||||
from_fiscal_year=prev_fy.name,
|
||||
to_fiscal_year=cur_fy.name,
|
||||
period_start_date=prev_fy.year_start_date,
|
||||
period_end_date=cur_fy.year_end_date,
|
||||
filter_based_on="Date Range",
|
||||
periodicity="Yearly",
|
||||
accumulated_values=False,
|
||||
)
|
||||
result = execute(filters)
|
||||
columns = [result[0][2], result[0][3]]
|
||||
expected = {
|
||||
"account": income_acc,
|
||||
columns[0].get("fieldname"): 450.0,
|
||||
columns[1].get("fieldname"): 120.0,
|
||||
}
|
||||
actual = [x for x in result[1] if x.get("account") == income_acc]
|
||||
self.assertEqual(len(actual), 1)
|
||||
actual = actual[0]
|
||||
for key in expected.keys():
|
||||
with self.subTest(key=key):
|
||||
self.assertEqual(expected.get(key), actual.get(key))
|
||||
|
||||
# accumualted
|
||||
filters.update({"accumulated_values": True})
|
||||
expected = {
|
||||
"account": income_acc,
|
||||
columns[0].get("fieldname"): 450.0,
|
||||
columns[1].get("fieldname"): 570.0,
|
||||
}
|
||||
result = execute(filters)
|
||||
columns = [result[0][2], result[0][3]]
|
||||
actual = [x for x in result[1] if x.get("account") == income_acc]
|
||||
self.assertEqual(len(actual), 1)
|
||||
actual = actual[0]
|
||||
for key in expected.keys():
|
||||
with self.subTest(key=key):
|
||||
self.assertEqual(expected.get(key), actual.get(key))
|
||||
|
||||
@@ -101,13 +101,18 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||
|
||||
for entry in gl_entries:
|
||||
transaction_currency = entry.get("transaction_currency")
|
||||
debit = flt(entry["debit"])
|
||||
credit = flt(entry["credit"])
|
||||
debit_in_account_currency = flt(entry["debit_in_account_currency"])
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and (transaction_currency is None or account_currency == transaction_currency)
|
||||
):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -12,8 +12,8 @@ DEFAULT_FILTERS = {
|
||||
|
||||
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}),
|
||||
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||
("Accounts Payable", {"range": "30, 60, 90, 120"}),
|
||||
("Accounts Receivable", {"range": "30, 60, 90, 120"}),
|
||||
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
||||
|
||||
@@ -713,10 +713,13 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", payment_entry.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
@@ -724,7 +727,7 @@ def update_reference_in_payment_entry(
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
|
||||
filters: { item_code: doc.item_code },
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
frm.custom_make_buttons = {
|
||||
"Asset Capitalization": "Asset Capitalization",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "item_code.image",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
@@ -204,8 +205,8 @@
|
||||
"fieldname": "purchase_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Purchase Date",
|
||||
"mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "disposal_date",
|
||||
@@ -595,7 +596,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-04-24 15:31:47.373274",
|
||||
"modified": "2025-05-20 00:44:06.229177",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -42,16 +42,15 @@ from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
class Asset(AccountsController):
|
||||
# begin: auto-generated types
|
||||
# ruff: noqa
|
||||
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
|
||||
additional_asset_cost: DF.Currency
|
||||
amended_from: DF.Link | None
|
||||
asset_category: DF.Link | None
|
||||
@@ -94,7 +93,7 @@ class Asset(AccountsController):
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
policy_number: DF.Data | None
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_date: DF.Date
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Data | None
|
||||
purchase_receipt: DF.Link | None
|
||||
@@ -118,10 +117,10 @@ class Asset(AccountsController):
|
||||
total_asset_cost: DF.Currency
|
||||
total_number_of_depreciations: DF.Int
|
||||
value_after_depreciation: DF.Currency
|
||||
# ruff: noqa
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_category()
|
||||
self.validate_precision()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
@@ -343,6 +342,17 @@ class Asset(AccountsController):
|
||||
title=_("Missing Finance Book"),
|
||||
)
|
||||
|
||||
def validate_category(self):
|
||||
non_depreciable_category = frappe.db.get_value(
|
||||
"Asset Category", self.asset_category, "non_depreciable_category"
|
||||
)
|
||||
if self.calculate_depreciation and non_depreciable_category:
|
||||
frappe.throw(
|
||||
_(
|
||||
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(
|
||||
|
||||
@@ -330,45 +330,6 @@ def _make_journal_entry_for_depreciation(
|
||||
row.db_update()
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset_category, company):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": asset_category, "company_name": company},
|
||||
fieldname=[
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
"depreciation_expense_account",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if accounts:
|
||||
fixed_asset_account = accounts.fixed_asset_account
|
||||
accumulated_depreciation_account = accounts.accumulated_depreciation_account
|
||||
depreciation_expense_account = accounts.depreciation_expense_account
|
||||
|
||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
accounts = frappe.get_cached_value(
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
if not accumulated_depreciation_account:
|
||||
accumulated_depreciation_account = accounts[0]
|
||||
if not depreciation_expense_account:
|
||||
depreciation_expense_account = accounts[1]
|
||||
|
||||
if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
frappe.throw(
|
||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||
asset_category, company
|
||||
)
|
||||
)
|
||||
|
||||
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
|
||||
|
||||
|
||||
def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
|
||||
root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
|
||||
|
||||
@@ -718,16 +679,15 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
return (
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -739,6 +699,52 @@ def get_asset_details(asset, finance_book=None):
|
||||
)
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset_category, company):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
non_depreciable_category = frappe.db.get_value(
|
||||
"Asset Category", asset_category, "non_depreciable_category"
|
||||
)
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": asset_category, "company_name": company},
|
||||
fieldname=[
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
"depreciation_expense_account",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if accounts:
|
||||
fixed_asset_account = accounts.fixed_asset_account
|
||||
accumulated_depreciation_account = accounts.accumulated_depreciation_account
|
||||
depreciation_expense_account = accounts.depreciation_expense_account
|
||||
|
||||
if not fixed_asset_account:
|
||||
frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category))
|
||||
|
||||
if not non_depreciable_category:
|
||||
accounts = frappe.get_cached_value(
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
if not accumulated_depreciation_account:
|
||||
accumulated_depreciation_account = accounts[0]
|
||||
if not depreciation_expense_account:
|
||||
depreciation_expense_account = accounts[1]
|
||||
|
||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
frappe.throw(
|
||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||
asset_category, company
|
||||
)
|
||||
)
|
||||
|
||||
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
|
||||
|
||||
|
||||
def get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||
):
|
||||
@@ -792,7 +798,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
||||
|
||||
idx = 1
|
||||
if finance_book:
|
||||
for d in asset.finance_books:
|
||||
for d in asset_doc.finance_books:
|
||||
if d.finance_book == finance_book:
|
||||
idx = d.idx
|
||||
break
|
||||
|
||||
@@ -281,7 +281,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
if (me.frm.doc.target_item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
item_code: me.frm.doc.target_item_code,
|
||||
company: me.frm.doc.company,
|
||||
@@ -301,7 +300,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
if (me.frm.doc.target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
asset: me.frm.doc.target_asset,
|
||||
company: me.frm.doc.company,
|
||||
@@ -404,7 +402,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: cstr(item.warehouse),
|
||||
qty: flt(item.stock_qty),
|
||||
qty: -1 * flt(item.stock_qty),
|
||||
serial_no: item.serial_no,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
|
||||
@@ -875,8 +875,8 @@ def get_items_tagged_to_wip_composite_asset(params):
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
"is_fixed_asset",
|
||||
"parent",
|
||||
"name",
|
||||
"parent as purchase_receipt",
|
||||
"name as purchase_receipt_item",
|
||||
]
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
@@ -905,7 +905,7 @@ def process_stock_item(d):
|
||||
stock_capitalized = frappe.db.exists(
|
||||
"Asset Capitalization Stock Item",
|
||||
{
|
||||
"purchase_receipt_item": d.name,
|
||||
"purchase_receipt_item": d.purchase_receipt_item,
|
||||
"parentfield": "stock_items",
|
||||
"parenttype": "Asset Capitalization",
|
||||
"docstatus": 1,
|
||||
@@ -916,7 +916,7 @@ def process_stock_item(d):
|
||||
return None
|
||||
|
||||
stock_item_data = frappe._dict(d)
|
||||
stock_item_data.purchase_receipt_item = d.name
|
||||
stock_item_data.purchase_receipt_item = d.purchase_receipt_item
|
||||
return stock_item_data
|
||||
|
||||
|
||||
@@ -925,7 +925,7 @@ def process_fixed_asset(d):
|
||||
"Asset",
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"purchase_receipt": d.parent,
|
||||
"purchase_receipt": d.purchase_receipt,
|
||||
"status": ("not in", ["Draft", "Scrapped", "Sold", "Capitalized"]),
|
||||
},
|
||||
["name as asset", "asset_name", "company"],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"column_break_3",
|
||||
"depreciation_options",
|
||||
"enable_cwip_accounting",
|
||||
"non_depreciable_category",
|
||||
"finance_book_detail",
|
||||
"finance_books",
|
||||
"section_break_2",
|
||||
@@ -63,10 +64,16 @@
|
||||
"fieldname": "enable_cwip_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Capital Work in Progress Accounting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "non_depreciable_category",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Depreciable Category"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-02-24 15:05:38.621803",
|
||||
"modified": "2025-05-13 15:33:03.791814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Category",
|
||||
@@ -111,8 +118,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,14 @@ class AssetCategory(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_category_account.asset_category_account import (
|
||||
AssetCategoryAccount,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category_account.asset_category_account import AssetCategoryAccount
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
|
||||
accounts: DF.Table[AssetCategoryAccount]
|
||||
asset_category_name: DF.Data
|
||||
enable_cwip_accounting: DF.Check
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
non_depreciable_category: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -255,8 +255,10 @@ class AssetDepreciationSchedule(Document):
|
||||
value_after_depreciation,
|
||||
):
|
||||
asset_doc.validate_asset_finance_books(row)
|
||||
|
||||
if not value_after_depreciation:
|
||||
if (
|
||||
not value_after_depreciation
|
||||
and not asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
|
||||
):
|
||||
value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
|
||||
row.value_after_depreciation = value_after_depreciation
|
||||
|
||||
@@ -1068,8 +1070,6 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
)
|
||||
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
||||
value_after_depreciation = row.value_after_depreciation - difference_amount
|
||||
|
||||
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
||||
"Written Down Value",
|
||||
|
||||
@@ -153,8 +153,6 @@ class AssetMovement(Document):
|
||||
args,
|
||||
)
|
||||
|
||||
self.validate_movement_cancellation(d, latest_movement_entry)
|
||||
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
@@ -182,12 +180,3 @@ class AssetMovement(Document):
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
|
||||
def validate_movement_cancellation(self, row, latest_movement_entry):
|
||||
asset_doc = frappe.get_doc("Asset", row.asset)
|
||||
if not latest_movement_entry and asset_doc.docstatus == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking."
|
||||
).format(row.asset)
|
||||
)
|
||||
|
||||
@@ -147,45 +147,6 @@ class TestAssetMovement(unittest.TestCase):
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_last_movement_cancellation_validation(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
asset.calculate_depreciation = 1
|
||||
asset.available_for_use_date = "2020-06-06"
|
||||
asset.purchase_date = "2020-06-06"
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"next_depreciation_date": "2020-12-31",
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 3,
|
||||
"frequency_of_depreciation": 10,
|
||||
},
|
||||
)
|
||||
if asset.docstatus == 0:
|
||||
asset.submit()
|
||||
|
||||
AssetMovement = frappe.qb.DocType("Asset Movement")
|
||||
AssetMovementItem = frappe.qb.DocType("Asset Movement Item")
|
||||
|
||||
asset_movement = (
|
||||
frappe.qb.from_(AssetMovement)
|
||||
.join(AssetMovementItem)
|
||||
.on(AssetMovementItem.parent == AssetMovement.name)
|
||||
.select(AssetMovement.name)
|
||||
.where(
|
||||
(AssetMovementItem.asset == asset.name)
|
||||
& (AssetMovement.company == asset.company)
|
||||
& (AssetMovement.docstatus == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name)
|
||||
self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel)
|
||||
|
||||
|
||||
def create_asset_movement(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, formatdate, get_link_to_form, getdate
|
||||
from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
@@ -188,15 +188,25 @@ class AssetValueAdjustment(Document):
|
||||
get_link_to_form(self.get("doctype"), self.get("name")),
|
||||
)
|
||||
|
||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
asset.db_update()
|
||||
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
asset,
|
||||
notes,
|
||||
value_after_depreciation=asset_value,
|
||||
ignore_booked_entry=True,
|
||||
difference_amount=self.difference_amount,
|
||||
difference_amount=difference_amount,
|
||||
)
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.save()
|
||||
asset.set_status()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -12,19 +12,26 @@
|
||||
"column_break_4",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"transaction_settings_section",
|
||||
"section_break_xmlt",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"blanket_order_allowance",
|
||||
"column_break_sbwq",
|
||||
"pr_required",
|
||||
"project_update_frequency",
|
||||
"transaction_settings_section",
|
||||
"column_break_fcyl",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"allow_zero_qty_in_supplier_quotation",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"allow_zero_qty_in_request_for_quotation",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"set_valuation_rate_for_rejected_materials",
|
||||
"disable_last_purchase_rate",
|
||||
"show_pay_button",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"allow_zero_qty_in_purchase_order",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@@ -207,14 +214,56 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Update frequency of Project",
|
||||
"options": "Each Transaction\nManual"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Purchase Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_purchase_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Purchase Order with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Request for Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_request_for_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Request for Quotation with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_supplier_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Supplier Quotation with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xmlt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sbwq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_fcyl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
|
||||
"fieldname": "set_valuation_rate_for_rejected_materials",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Valuation Rate for Rejected Materials"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-31 13:34:18.101256",
|
||||
"modified": "2025-05-16 15:56:38.321369",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -18,6 +18,9 @@ class BuyingSettings(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
allow_multiple_items: DF.Check
|
||||
allow_zero_qty_in_purchase_order: DF.Check
|
||||
allow_zero_qty_in_request_for_quotation: DF.Check
|
||||
allow_zero_qty_in_supplier_quotation: DF.Check
|
||||
auto_create_purchase_receipt: DF.Check
|
||||
auto_create_subcontracting_order: DF.Check
|
||||
backflush_raw_materials_of_subcontract_based_on: DF.Literal[
|
||||
@@ -35,6 +38,7 @@ class BuyingSettings(Document):
|
||||
project_update_frequency: DF.Literal["Each Transaction", "Manual"]
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
set_landed_cost_based_on_purchase_invoice_rate: DF.Check
|
||||
set_valuation_rate_for_rejected_materials: DF.Check
|
||||
show_pay_button: DF.Check
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
@@ -54,6 +58,9 @@ class BuyingSettings(Document):
|
||||
hide_name_field=False,
|
||||
)
|
||||
|
||||
if not self.bill_for_rejected_quantity_in_purchase_invoice:
|
||||
self.set_valuation_rate_for_rejected_materials = 0
|
||||
|
||||
def before_save(self):
|
||||
self.check_maintain_same_rate()
|
||||
|
||||
|
||||
@@ -26,7 +26,15 @@ frappe.ui.form.on("Purchase Order", {
|
||||
}
|
||||
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return doc.qty <= doc.received_qty ? "green" : "orange";
|
||||
let color;
|
||||
if (!doc.qty && frm.doc.has_unit_price_items) {
|
||||
color = "yellow";
|
||||
} else if (doc.qty <= doc.received_qty) {
|
||||
color = "green";
|
||||
} else {
|
||||
color = "orange";
|
||||
}
|
||||
return color;
|
||||
});
|
||||
|
||||
frm.set_query("expense_account", "items", function () {
|
||||
@@ -63,6 +71,10 @@ frappe.ui.form.on("Purchase Order", {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 0) {
|
||||
erpnext.set_unit_price_items_note(frm);
|
||||
}
|
||||
},
|
||||
|
||||
supplier: function (frm) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"is_subcontracted",
|
||||
"has_unit_price_items",
|
||||
"supplier_warehouse",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
@@ -1285,6 +1286,14 @@
|
||||
"label": "Dispatch Address Details",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_unit_price_items",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -97,6 +97,7 @@ class PurchaseOrder(BuyingController):
|
||||
from_date: DF.Date | None
|
||||
grand_total: DF.Currency
|
||||
group_same_items: DF.Check
|
||||
has_unit_price_items: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
in_words: DF.Data | None
|
||||
incoterm: DF.Link | None
|
||||
@@ -191,6 +192,10 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
self.set_onload("can_update_items", self.can_update_items())
|
||||
|
||||
def before_validate(self):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
@@ -223,6 +228,17 @@ class PurchaseOrder(BuyingController):
|
||||
)
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def set_has_unit_price_items(self):
|
||||
"""
|
||||
If permitted in settings and any item has 0 qty, the PO has unit price items.
|
||||
"""
|
||||
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order"):
|
||||
return
|
||||
|
||||
self.has_unit_price_items = any(
|
||||
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
mri_compare_fields = [["project", "="], ["item_code", "="]]
|
||||
if self.is_subcontracted:
|
||||
@@ -707,8 +723,13 @@ def set_missing_values(source, target):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
|
||||
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.qty = flt(obj.qty) if is_unit_price_row(obj) else flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
@@ -739,7 +760,9 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||
"condition": lambda doc: (
|
||||
True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty)
|
||||
)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
|
||||
@@ -31,6 +31,8 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_purchase_order_qty(self):
|
||||
po = create_purchase_order(qty=1, do_not_save=True)
|
||||
|
||||
# NonNegativeError with qty=-1
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
@@ -41,9 +43,22 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
self.assertRaises(frappe.NonNegativeError, po.save)
|
||||
|
||||
# InvalidQtyError with qty=0
|
||||
po.items[1].qty = 0
|
||||
self.assertRaises(InvalidQtyError, po.save)
|
||||
|
||||
# No error with qty=1
|
||||
po.items[1].qty = 1
|
||||
po.save()
|
||||
self.assertEqual(po.items[1].qty, 1)
|
||||
|
||||
def test_purchase_order_zero_qty(self):
|
||||
po = create_purchase_order(qty=0, do_not_save=True)
|
||||
|
||||
with change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}):
|
||||
po.save()
|
||||
self.assertEqual(po.items[0].qty, 0)
|
||||
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
@@ -793,8 +808,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
po_doc.reload()
|
||||
self.assertEqual(po_doc.advance_paid, 5000)
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
company_doc.book_advance_payments_in_separate_party_account = False
|
||||
company_doc.save()
|
||||
|
||||
@@ -1199,6 +1212,80 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
po.reload()
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
@change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1})
|
||||
def test_receive_zero_qty_purchase_order(self):
|
||||
"""
|
||||
Test the flow of a Unit Price PO and PR creation against it until completion.
|
||||
Flow:
|
||||
PO Qty 0 -> Receive +5 -> Receive +5 -> Update PO Qty +10 -> PO is 100% received
|
||||
"""
|
||||
po = create_purchase_order(qty=0)
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
self.assertEqual(pr.items[0].qty, 0)
|
||||
pr.items[0].qty = 5
|
||||
pr.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].received_qty, 5)
|
||||
self.assertFalse(po.per_received)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
# Update PO Item Qty to 10 after receipt of items
|
||||
first_item_of_po = po.items[0]
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": first_item_of_po.item_code,
|
||||
"rate": first_item_of_po.rate,
|
||||
"qty": 10,
|
||||
"docname": first_item_of_po.name,
|
||||
}
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Purchase Order", trans_item, po.name)
|
||||
|
||||
# Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty
|
||||
pr2 = make_purchase_receipt(po.name)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].qty, 10)
|
||||
self.assertEqual(pr2.items[0].qty, 5)
|
||||
|
||||
pr2.submit()
|
||||
|
||||
# PO should be updated to 100% received
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].qty, 10)
|
||||
self.assertEqual(po.items[0].received_qty, 10)
|
||||
self.assertEqual(po.per_received, 100.0)
|
||||
self.assertEqual(po.status, "To Bill")
|
||||
|
||||
@change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1})
|
||||
def test_bill_zero_qty_purchase_order(self):
|
||||
po = create_purchase_order(qty=0)
|
||||
|
||||
self.assertEqual(po.grand_total, 0)
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.items[0].qty, 0)
|
||||
self.assertEqual(po.items[0].rate, 500)
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 0)
|
||||
self.assertEqual(pi.items[0].rate, 500)
|
||||
|
||||
pi.items[0].qty = 5
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pi.grand_total, 2500)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].amount, 0)
|
||||
self.assertEqual(po.items[0].billed_amt, 2500)
|
||||
# PO still has qty 0, so billed % should be unset
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@@ -1341,7 +1428,7 @@ def create_purchase_order(**args):
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"from_warehouse": args.from_warehouse,
|
||||
"qty": args.qty or 10,
|
||||
"qty": args.qty if args.qty is not None else 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get("include_exploded_items", 1),
|
||||
|
||||
@@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
is_group: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -163,6 +167,10 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
erpnext.set_unit_price_items_note(frm);
|
||||
}
|
||||
},
|
||||
|
||||
show_supplier_quotation_comparison(frm) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"status",
|
||||
"has_unit_price_items",
|
||||
"amended_from",
|
||||
"suppliers_section",
|
||||
"suppliers",
|
||||
@@ -306,13 +307,22 @@
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Billing Address Details",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_unit_price_items",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-06 12:45:28.898706",
|
||||
"modified": "2025-03-03 16:48:39.856779",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
@@ -377,6 +387,7 @@
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status, transaction_date",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController):
|
||||
billing_address_display: DF.SmallText | None
|
||||
company: DF.Link
|
||||
email_template: DF.Link | None
|
||||
has_unit_price_items: DF.Check
|
||||
incoterm: DF.Link | None
|
||||
items: DF.Table[RequestforQuotationItem]
|
||||
letter_head: DF.Link | None
|
||||
@@ -61,9 +62,14 @@ class RequestforQuotation(BuyingController):
|
||||
vendor: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
def validate(self):
|
||||
self.validate_duplicate_supplier()
|
||||
self.validate_supplier_list()
|
||||
super().validate_qty_is_not_zero()
|
||||
validate_for_items(self)
|
||||
super().set_qty_as_per_stock_uom()
|
||||
self.update_email_id()
|
||||
@@ -72,6 +78,17 @@ class RequestforQuotation(BuyingController):
|
||||
# after amend and save, status still shows as cancelled, until submit
|
||||
self.db_set("status", "Draft")
|
||||
|
||||
def set_has_unit_price_items(self):
|
||||
"""
|
||||
If permitted in settings and any item has 0 qty, the RFQ has unit price items.
|
||||
"""
|
||||
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"):
|
||||
return
|
||||
|
||||
self.has_unit_price_items = any(
|
||||
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
|
||||
)
|
||||
|
||||
def validate_duplicate_supplier(self):
|
||||
supplier_list = [d.supplier for d in self.suppliers]
|
||||
if len(supplier_list) != len(set(supplier_list)):
|
||||
@@ -439,11 +456,10 @@ def create_supplier_quotation(doc):
|
||||
|
||||
def add_items(sq_doc, supplier, items):
|
||||
for data in items:
|
||||
if data.get("qty") > 0:
|
||||
if isinstance(data, dict):
|
||||
data = frappe._dict(data)
|
||||
if isinstance(data, dict):
|
||||
data = frappe._dict(data)
|
||||
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
@@ -14,6 +14,7 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
get_pdf,
|
||||
make_supplier_quotation_from_rfq,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
|
||||
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -21,6 +22,26 @@ from erpnext.templates.pages.rfq import check_supplier_has_docname_access
|
||||
|
||||
|
||||
class TestRequestforQuotation(FrappeTestCase):
|
||||
def test_rfq_qty(self):
|
||||
rfq = make_request_for_quotation(qty=0, do_not_save=True)
|
||||
with self.assertRaises(InvalidQtyError):
|
||||
rfq.save()
|
||||
|
||||
# No error with qty=1
|
||||
rfq.items[0].qty = 1
|
||||
rfq.save()
|
||||
self.assertEqual(rfq.items[0].qty, 1)
|
||||
|
||||
def test_rfq_zero_qty(self):
|
||||
"""
|
||||
Test if RFQ with zero qty (Unit Price Item) is conditionally allowed.
|
||||
"""
|
||||
rfq = make_request_for_quotation(qty=0, do_not_save=True)
|
||||
|
||||
with change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}):
|
||||
rfq.save()
|
||||
self.assertEqual(rfq.items[0].qty, 0)
|
||||
|
||||
def test_quote_status(self):
|
||||
rfq = make_request_for_quotation()
|
||||
|
||||
@@ -161,6 +182,32 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
supplier_doc.reload()
|
||||
self.assertTrue(supplier_doc.portal_users[0].user)
|
||||
|
||||
@change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1})
|
||||
def test_supplier_quotation_from_zero_qty_rfq(self):
|
||||
rfq = make_request_for_quotation(qty=0)
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier)
|
||||
|
||||
self.assertEqual(len(sq.items), 1)
|
||||
self.assertEqual(sq.items[0].qty, 0)
|
||||
self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code)
|
||||
|
||||
@change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"allow_zero_qty_in_request_for_quotation": 1,
|
||||
"allow_zero_qty_in_supplier_quotation": 1,
|
||||
},
|
||||
)
|
||||
def test_supplier_quotation_from_zero_qty_rfq_in_portal(self):
|
||||
rfq = make_request_for_quotation(qty=0)
|
||||
rfq.supplier = rfq.suppliers[0].supplier
|
||||
sq_name = create_supplier_quotation(rfq)
|
||||
|
||||
sq = frappe.get_doc("Supplier Quotation", sq_name)
|
||||
self.assertEqual(len(sq.items), 1)
|
||||
self.assertEqual(sq.items[0].qty, 0)
|
||||
self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code)
|
||||
|
||||
|
||||
def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
"""
|
||||
@@ -184,14 +231,17 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
"description": "_Test Item",
|
||||
"uom": args.uom or "_Test UOM",
|
||||
"stock_uom": args.stock_uom or "_Test UOM",
|
||||
"qty": args.qty or 5,
|
||||
"qty": args.qty if args.qty is not None else 5,
|
||||
"conversion_factor": args.conversion_factor or 1.0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
},
|
||||
)
|
||||
|
||||
rfq.submit()
|
||||
if not args.do_not_save:
|
||||
rfq.insert()
|
||||
if not args.do_not_submit:
|
||||
rfq.submit()
|
||||
|
||||
return rfq
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
Quotation: "Quotation",
|
||||
};
|
||||
|
||||
const me = this;
|
||||
this.frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
|
||||
});
|
||||
|
||||
super.setup();
|
||||
}
|
||||
|
||||
@@ -26,6 +31,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
} else if (this.frm.doc.docstatus === 0) {
|
||||
erpnext.set_unit_price_items_note(this.frm);
|
||||
|
||||
this.frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
function () {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"transaction_date",
|
||||
"valid_till",
|
||||
"quotation_number",
|
||||
"has_unit_price_items",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
@@ -921,14 +922,23 @@
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_unit_price_items",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"idx": 29,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-28 10:20:30.231915",
|
||||
"modified": "2025-03-03 17:39:38.459977",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
@@ -989,6 +999,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status, transaction_date, supplier,grand_total",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController):
|
||||
discount_amount: DF.Currency
|
||||
grand_total: DF.Currency
|
||||
group_same_items: DF.Check
|
||||
has_unit_price_items: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
in_words: DF.Data | None
|
||||
incoterm: DF.Link | None
|
||||
@@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController):
|
||||
valid_till: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
@@ -129,6 +134,17 @@ class SupplierQuotation(BuyingController):
|
||||
def on_trash(self):
|
||||
pass
|
||||
|
||||
def set_has_unit_price_items(self):
|
||||
"""
|
||||
If permitted in settings and any item has 0 qty, the SQ has unit price items.
|
||||
"""
|
||||
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_supplier_quotation"):
|
||||
return
|
||||
|
||||
self.has_unit_price_items = any(
|
||||
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
|
||||
@@ -3,14 +3,26 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_make_purchase_order(self):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
def test_supplier_quotation_qty(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.items[0].qty = 0
|
||||
with self.assertRaises(InvalidQtyError):
|
||||
sq.save()
|
||||
|
||||
# No error with qty=1
|
||||
sq.items[0].qty = 1
|
||||
sq.save()
|
||||
self.assertEqual(sq.items[0].qty, 1)
|
||||
|
||||
def test_make_purchase_order(self):
|
||||
sq = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_order, sq.name)
|
||||
@@ -30,5 +42,16 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
po.insert()
|
||||
|
||||
@change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1})
|
||||
def test_map_purchase_order_from_zero_qty_supplier_quotation(self):
|
||||
sq = frappe.copy_doc(test_records[0]).insert()
|
||||
sq.items[0].qty = 0
|
||||
sq.submit()
|
||||
|
||||
po = make_purchase_order(sq.name)
|
||||
self.assertEqual(len(po.get("items")), 1)
|
||||
self.assertEqual(po.get("items")[0].qty, 0)
|
||||
self.assertEqual(po.get("items")[0].item_code, sq.get("items")[0].item_code)
|
||||
|
||||
|
||||
test_records = frappe.get_test_records("Supplier Quotation")
|
||||
|
||||
@@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
width: "80",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("company"),
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
|
||||
@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ label: __("Group by Supplier"), value: "Group by Supplier" },
|
||||
{ label: __("Group by Item"), value: "Group by Item" },
|
||||
{ label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
|
||||
{ label: __("Categorize by Item"), value: "Categorize by Item" },
|
||||
],
|
||||
default: __("Group by Supplier"),
|
||||
default: __("Categorize by Supplier"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
|
||||
@@ -15,6 +15,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
return [], []
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
supplier_quotation_data = get_data(filters)
|
||||
|
||||
@@ -24,6 +26,12 @@ def execute(filters=None):
|
||||
return columns, data, message, chart_data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||
filters["categorize_by"] = filters["group_by"]
|
||||
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
sq = frappe.qb.DocType("Supplier Quotation")
|
||||
sq_item = frappe.qb.DocType("Supplier Quotation Item")
|
||||
@@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters):
|
||||
group_wise_map = defaultdict(list)
|
||||
supplier_qty_price_map = {}
|
||||
|
||||
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
|
||||
group_by_field = (
|
||||
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
|
||||
)
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
for data in supplier_quotation_data:
|
||||
@@ -266,7 +276,7 @@ def get_columns(filters):
|
||||
},
|
||||
]
|
||||
|
||||
if filters.get("group_by") == "Group by Item":
|
||||
if filters.get("categorize_by") == "Categorize by Item":
|
||||
group_by_columns.reverse()
|
||||
|
||||
columns[0:0] = group_by_columns # add positioned group by columns to the report
|
||||
|
||||
@@ -20,6 +20,9 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
if d.get("is_free_item"):
|
||||
continue
|
||||
|
||||
# get last purchase details
|
||||
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
|
||||
|
||||
@@ -46,11 +49,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
def validate_for_items(doc) -> None:
|
||||
items = []
|
||||
for d in doc.get("items"):
|
||||
if not d.qty:
|
||||
if doc.doctype == "Purchase Receipt" and d.rejected_qty:
|
||||
continue
|
||||
frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
|
||||
|
||||
set_stock_levels(row=d) # update with latest quantities
|
||||
item = validate_item_and_get_basic_data(row=d)
|
||||
validate_stock_item_warehouse(row=d, item=item)
|
||||
|
||||
11
erpnext/change_log/v15/v15_64_0.md
Normal file
11
erpnext/change_log/v15/v15_64_0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
There was a bug in the **Additional Discount** functionality of ERPNext in **v15.64.0**. This has since been fixed.
|
||||
|
||||
**If you've updated from a version older than v15.64.0, no action is needed on your side.**
|
||||
|
||||
If you're updating from v15.64.0, the **Additional Discount Amount** in some transactions may differ from the value you entered. This only affects cases where **Additional Discount Amount** is manually entered. If it is computed from **Additional Discount Percentage** entered by you, there shouldn't be any issue.
|
||||
|
||||
This report can help identify such transactions: [Calculated Discount Mismatch](/app/query-report/Calculated%20Discount%20Mismatch)
|
||||
|
||||
Please review and amend these as necessary.
|
||||
|
||||
We apologize for the inconvenience caused.
|
||||
@@ -230,6 +230,8 @@ class AccountsController(TransactionBase):
|
||||
self.validate_party_accounts()
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
# validate inter company transaction rate
|
||||
self.validate_internal_transaction()
|
||||
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
@@ -649,6 +651,9 @@ class AccountsController(TransactionBase):
|
||||
self.base_paid_amount = flt(
|
||||
self.paid_amount * self.conversion_rate, self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
self.paid_amount = 0
|
||||
self.base_paid_amount = 0
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
if frappe.flags.in_test:
|
||||
@@ -737,6 +742,91 @@ class AccountsController(TransactionBase):
|
||||
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
|
||||
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
|
||||
|
||||
def validate_internal_transaction(self):
|
||||
if not cint(
|
||||
frappe.db.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")
|
||||
):
|
||||
return
|
||||
|
||||
doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
|
||||
if self.doctype in doctypes_list and (
|
||||
self.get("is_internal_customer") or self.get("is_internal_supplier")
|
||||
):
|
||||
self.validate_internal_transaction_based_on_voucher_type()
|
||||
|
||||
def validate_internal_transaction_based_on_voucher_type(self):
|
||||
order = ["Sales Order", "Purchase Order"]
|
||||
invoice = ["Sales Invoice", "Purchase Invoice"]
|
||||
|
||||
if self.doctype in order and self.get("inter_company_order_reference"):
|
||||
# Fetch the linked order
|
||||
linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order"
|
||||
self.validate_line_items(
|
||||
linked_doctype,
|
||||
"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
|
||||
"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
|
||||
)
|
||||
elif self.doctype in invoice and self.get("inter_company_invoice_reference"):
|
||||
# Fetch the linked invoice
|
||||
linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice"
|
||||
self.validate_line_items(
|
||||
linked_doctype,
|
||||
"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
|
||||
"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
|
||||
)
|
||||
|
||||
def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field):
|
||||
action, role_allowed_to_override = frappe.get_cached_value(
|
||||
"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||
)
|
||||
|
||||
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||
|
||||
stop_actions = []
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.get(ref_link_field):
|
||||
ref_rate = reference_details.get(d.get(ref_link_field))
|
||||
if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||
if action == "Stop":
|
||||
user_roles = [
|
||||
r["role"]
|
||||
for r in frappe.get_all(
|
||||
"Has Role", filters={"parent": frappe.session.user}, fields=["role"]
|
||||
)
|
||||
]
|
||||
if role_allowed_to_override not in user_roles:
|
||||
stop_actions.append(
|
||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx,
|
||||
ref_dt,
|
||||
self.inter_company_invoice_reference
|
||||
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
|
||||
else d.get(ref_dn_field),
|
||||
d.rate,
|
||||
ref_rate,
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx,
|
||||
ref_dt,
|
||||
self.inter_company_invoice_reference
|
||||
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
|
||||
else d.get(ref_dn_field),
|
||||
d.rate,
|
||||
ref_rate,
|
||||
),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
if stop_actions:
|
||||
frappe.throw(stop_actions, as_list=True)
|
||||
|
||||
def disable_pricing_rule_on_internal_transfer(self):
|
||||
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||
self.ignore_pricing_rule = 1
|
||||
@@ -779,17 +869,10 @@ class AccountsController(TransactionBase):
|
||||
if not self.due_date:
|
||||
frappe.throw(_("Due Date is mandatory"))
|
||||
|
||||
validate_due_date(
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.payment_terms_template,
|
||||
)
|
||||
validate_due_date(posting_date, self.due_date, None, self.payment_terms_template, self.doctype)
|
||||
elif self.doctype == "Purchase Invoice":
|
||||
validate_due_date(
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.bill_date,
|
||||
self.payment_terms_template,
|
||||
posting_date, self.due_date, self.bill_date, self.payment_terms_template, self.doctype
|
||||
)
|
||||
|
||||
def set_price_list_currency(self, buying_or_selling):
|
||||
@@ -1155,6 +1238,8 @@ class AccountsController(TransactionBase):
|
||||
with temporary_flag("company", self.company):
|
||||
update_gl_dict_with_regional_fields(self, gl_dict)
|
||||
|
||||
update_gl_dict_with_app_based_fields(self, gl_dict)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
dimension_dict = frappe._dict()
|
||||
|
||||
@@ -1259,13 +1344,18 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
if self.doctype == "Purchase Receipt":
|
||||
if self.flags.allow_zero_qty:
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if self.doctype == "Purchase Receipt" and item.rejected_qty:
|
||||
continue
|
||||
|
||||
if not flt(item.qty):
|
||||
frappe.throw(
|
||||
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
|
||||
msg=_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
title=_("Invalid Quantity"),
|
||||
exc=InvalidQtyError,
|
||||
)
|
||||
@@ -3596,7 +3686,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
def validate_quantity(child_item, new_data):
|
||||
if not flt(new_data.get("qty")):
|
||||
frappe.throw(
|
||||
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
|
||||
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
@@ -3737,9 +3827,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
|
||||
child_item.idx, child_item.item_code
|
||||
)
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
)
|
||||
else:
|
||||
child_item.rate = row_rate
|
||||
@@ -3936,3 +4026,8 @@ def validate_einvoice_fields(doc):
|
||||
@erpnext.allow_regional
|
||||
def update_gl_dict_with_regional_fields(doc, gl_dict):
|
||||
pass
|
||||
|
||||
|
||||
def update_gl_dict_with_app_based_fields(doc, gl_dict):
|
||||
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
|
||||
frappe.get_attr(method)(doc, gl_dict)
|
||||
|
||||
@@ -70,6 +70,14 @@ class BuyingController(SubcontractingController):
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"),
|
||||
)
|
||||
|
||||
if self.docstatus == 1 and self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
self.set_onload(
|
||||
"allow_to_make_qc_after_submission",
|
||||
frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
),
|
||||
)
|
||||
|
||||
def create_package_for_transfer(self) -> None:
|
||||
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
|
||||
|
||||
@@ -650,6 +658,10 @@ class BuyingController(SubcontractingController):
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
if flt(d.rejected_qty) != 0:
|
||||
valuation_rate_for_rejected_item = 0.0
|
||||
if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"):
|
||||
valuation_rate_for_rejected_item = d.valuation_rate
|
||||
|
||||
sl_entries.append(
|
||||
self.get_sl_entries(
|
||||
d,
|
||||
@@ -658,7 +670,8 @@ class BuyingController(SubcontractingController):
|
||||
"actual_qty": flt(
|
||||
flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")
|
||||
),
|
||||
"incoming_rate": 0.0,
|
||||
"incoming_rate": valuation_rate_for_rejected_item if not self.is_return else 0.0,
|
||||
"outgoing_rate": valuation_rate_for_rejected_item if self.is_return else 0.0,
|
||||
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -394,7 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
page_len = 30
|
||||
page_len = 300
|
||||
|
||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
||||
|
||||
@@ -342,7 +342,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
def make_return_doc(doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
company = frappe.db.get_value("Delivery Note", source_name, "company")
|
||||
company = frappe.db.get_value(doctype, source_name, "company")
|
||||
default_warehouse_for_sales_return = frappe.get_cached_value(
|
||||
"Company", company, "default_warehouse_for_sales_return"
|
||||
)
|
||||
@@ -431,99 +431,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
else:
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
returned_serial_nos = []
|
||||
returned_batches = frappe._dict()
|
||||
serial_and_batch_field = (
|
||||
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
|
||||
old_batch_no_field = "batch_no"
|
||||
|
||||
if (
|
||||
source_doc.get(serial_and_batch_field)
|
||||
or source_doc.get(old_serial_no_field)
|
||||
or source_doc.get(old_batch_no_field)
|
||||
):
|
||||
if item_details.has_serial_no:
|
||||
returned_serial_nos = get_returned_serial_nos(
|
||||
source_doc, source_parent, serial_no_field=serial_and_batch_field
|
||||
)
|
||||
else:
|
||||
returned_batches = get_returned_batches(
|
||||
source_doc, source_parent, batch_no_field=serial_and_batch_field
|
||||
)
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if source_doc.get(serial_and_batch_field) and (
|
||||
frappe.db.get_value(
|
||||
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
|
||||
)
|
||||
== "Inward"
|
||||
):
|
||||
type_of_transaction = "Outward"
|
||||
elif source_parent.doctype in [
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Subcontracting Receipt",
|
||||
]:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
|
||||
if source_parent.doctype in [
|
||||
"Sales Invoice",
|
||||
"POS Invoice",
|
||||
"Delivery Note",
|
||||
] and source_parent.get("is_internal_customer"):
|
||||
type_of_transaction = "Outward"
|
||||
warehouse = source_doc.target_warehouse
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
|
||||
"returned_against": source_doc.name,
|
||||
"item_code": source_doc.item_code,
|
||||
"returned_serial_nos": returned_serial_nos,
|
||||
"voucher_type": source_parent.doctype,
|
||||
"do_not_submit": True,
|
||||
"warehouse": warehouse,
|
||||
"has_serial_no": item_details.has_serial_no,
|
||||
"has_batch_no": item_details.has_batch_no,
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos = []
|
||||
batches = frappe._dict()
|
||||
if source_doc.get(old_batch_no_field):
|
||||
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
|
||||
elif source_doc.get(old_serial_no_field):
|
||||
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
|
||||
elif source_doc.get(serial_and_batch_field):
|
||||
if item_details.has_serial_no:
|
||||
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
|
||||
else:
|
||||
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
|
||||
|
||||
if serial_nos:
|
||||
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
|
||||
elif batches:
|
||||
for batch in batches:
|
||||
if batch in returned_batches:
|
||||
batches[batch] -= flt(returned_batches.get(batch))
|
||||
|
||||
cls_obj.batches = batches
|
||||
|
||||
if source_doc.get(serial_and_batch_field):
|
||||
cls_obj.duplicate_package()
|
||||
if cls_obj.serial_and_batch_bundle:
|
||||
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
|
||||
else:
|
||||
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
target_doc.pricing_rules = None
|
||||
@@ -866,8 +773,6 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_
|
||||
|
||||
|
||||
def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None):
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
|
||||
batches = frappe._dict()
|
||||
|
||||
old_field = "batch_no"
|
||||
|
||||
@@ -31,6 +31,14 @@ class SellingController(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
if self.docstatus == 1 and self.doctype in ["Delivery Note", "Sales Invoice"]:
|
||||
self.set_onload(
|
||||
"allow_to_make_qc_after_submission",
|
||||
frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_items()
|
||||
@@ -316,9 +324,6 @@ class SellingController(StockController):
|
||||
def get_item_list(self):
|
||||
il = []
|
||||
for d in self.get("items"):
|
||||
if d.qty is None:
|
||||
frappe.throw(_("Row {0}: Qty is mandatory").format(d.idx))
|
||||
|
||||
if self.has_product_bundle(d.item_code):
|
||||
for p in self.get("packed_items"):
|
||||
if p.parent_detail_docname == d.name and p.parent_item == d.item_code:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user