mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-10 00:42:45 +00:00
Compare commits
244 Commits
future_sub
...
pot_develo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd5c4cca0 | ||
|
|
9e9296e444 | ||
|
|
24926ab665 | ||
|
|
169d77da54 | ||
|
|
666232fd1c | ||
|
|
ee846f5950 | ||
|
|
4e37ed9033 | ||
|
|
07c421228b | ||
|
|
c6c696247a | ||
|
|
968120d0eb | ||
|
|
be9f960705 | ||
|
|
3f2343614b | ||
|
|
388ba7f945 | ||
|
|
289d216552 | ||
|
|
1c9fe691ea | ||
|
|
c41b239881 | ||
|
|
9f85483199 | ||
|
|
006b1f553c | ||
|
|
6b0ce33030 | ||
|
|
ce834f5dba | ||
|
|
e3cf53a8b7 | ||
|
|
d3bdd41358 | ||
|
|
eaa4efbc45 | ||
|
|
06401cc84f | ||
|
|
e3fd82766f | ||
|
|
537e0e32b2 | ||
|
|
47bc5691a1 | ||
|
|
c5c4d5c61f | ||
|
|
25931c4db2 | ||
|
|
683c1f04c8 | ||
|
|
b6350c86d1 | ||
|
|
fac0fcb9d5 | ||
|
|
3f06f1905f | ||
|
|
954ef85b95 | ||
|
|
fffcb5dc9f | ||
|
|
5d0e82f40b | ||
|
|
e2ec3e453a | ||
|
|
0303d7dbdc | ||
|
|
418439b58f | ||
|
|
e39d2e1e69 | ||
|
|
c4e2abb973 | ||
|
|
014b542cf3 | ||
|
|
cb3c20dcd3 | ||
|
|
ad817cddad | ||
|
|
04a49cef24 | ||
|
|
85b1a8001b | ||
|
|
3b4913ec81 | ||
|
|
d7df2cbdc5 | ||
|
|
ac4bcd5ca7 | ||
|
|
4261c3474b | ||
|
|
4e4c1e4bef | ||
|
|
ae3f5a38e1 | ||
|
|
dfdb95bbbe | ||
|
|
a1d489195d | ||
|
|
f24d61daea | ||
|
|
0191727149 | ||
|
|
e22b0fd6e1 | ||
|
|
91c9d964f9 | ||
|
|
6e3782a061 | ||
|
|
eb4f94ddf2 | ||
|
|
be1fce2da6 | ||
|
|
30aa4e031d | ||
|
|
214b38f7c8 | ||
|
|
28e10dd703 | ||
|
|
f7eb9d44a8 | ||
|
|
b071e952dd | ||
|
|
7b9fd394b0 | ||
|
|
7ce8fc9ba2 | ||
|
|
63c2cf13fd | ||
|
|
37d97fad17 | ||
|
|
5cf5b18aea | ||
|
|
1c0a24424a | ||
|
|
61a4188440 | ||
|
|
3f6787b267 | ||
|
|
deb9766f2a | ||
|
|
d61f56207c | ||
|
|
76131f8e10 | ||
|
|
cafa2f52e5 | ||
|
|
e883d6a7d3 | ||
|
|
44e6563af6 | ||
|
|
7a3f8a5afb | ||
|
|
8a30701893 | ||
|
|
500c546691 | ||
|
|
af8275c321 | ||
|
|
bd381cc0c6 | ||
|
|
8ac11ae88d | ||
|
|
2761705c62 | ||
|
|
000c1b49dc | ||
|
|
070e2d4d26 | ||
|
|
3c1ca1cfbb | ||
|
|
81a9521f04 | ||
|
|
0363afcfd0 | ||
|
|
7a47b2157b | ||
|
|
abfb8a7201 | ||
|
|
fc797836af | ||
|
|
7d86881579 | ||
|
|
d22df324ec | ||
|
|
4a4c674553 | ||
|
|
7b264e5e11 | ||
|
|
c0c8c1bb02 | ||
|
|
d40b55468c | ||
|
|
fccd37d32d | ||
|
|
b8a98a273b | ||
|
|
99d417bbc2 | ||
|
|
f337392f3e | ||
|
|
e843683ad1 | ||
|
|
a36b7fb95a | ||
|
|
16751ec935 | ||
|
|
0736154074 | ||
|
|
fa2b6c4490 | ||
|
|
3bd455ac85 | ||
|
|
d6fe220fab | ||
|
|
e3f539325e | ||
|
|
e3a80ebdf3 | ||
|
|
3309359822 | ||
|
|
6bd13d7452 | ||
|
|
13a3a6a82e | ||
|
|
64333373ca | ||
|
|
c6fd42fe60 | ||
|
|
77bfea975f | ||
|
|
663fcb374d | ||
|
|
065163146c | ||
|
|
d56f52b0ba | ||
|
|
32a463652e | ||
|
|
7f2062e2c5 | ||
|
|
98e7dfe97f | ||
|
|
6b24143f72 | ||
|
|
d3200fb67f | ||
|
|
7501fe8ebd | ||
|
|
3fa8706f57 | ||
|
|
f92aabd0f3 | ||
|
|
628d7e6458 | ||
|
|
360c3b36ed | ||
|
|
31841b4ab2 | ||
|
|
ba60b5911a | ||
|
|
501c6aa126 | ||
|
|
cfde8088b4 | ||
|
|
249e8264dd | ||
|
|
08899d05fb | ||
|
|
754c7f6d1c | ||
|
|
92673d706c | ||
|
|
9c3a105f85 | ||
|
|
418bdc1dcc | ||
|
|
c29d955371 | ||
|
|
272ce24016 | ||
|
|
651e4696fd | ||
|
|
5ed1b6b8fb | ||
|
|
cd5d244d2c | ||
|
|
db5ed972e5 | ||
|
|
0887161f2a | ||
|
|
544fc60093 | ||
|
|
c50663c640 | ||
|
|
c41a037174 | ||
|
|
c710ad822c | ||
|
|
ebfbe94aaf | ||
|
|
e51ccecd8a | ||
|
|
bd8382c592 | ||
|
|
6142d07f1a | ||
|
|
ed5344d58b | ||
|
|
44228b754f | ||
|
|
d19ae4ce4c | ||
|
|
58f7039630 | ||
|
|
eac7be2d0f | ||
|
|
42ef95759d | ||
|
|
eb31017058 | ||
|
|
69d68592ad | ||
|
|
eb9f579b8f | ||
|
|
e4db0562ac | ||
|
|
a61148c464 | ||
|
|
7fa22069d8 | ||
|
|
54a12c5186 | ||
|
|
c8e92cb1b2 | ||
|
|
93c6324115 | ||
|
|
f26be2a303 | ||
|
|
61f5ebd2dd | ||
|
|
22c345a24a | ||
|
|
9bf37426c1 | ||
|
|
1cbc200770 | ||
|
|
8289f3c724 | ||
|
|
da3010a41f | ||
|
|
ed9af7d12e | ||
|
|
bf755fab55 | ||
|
|
2402447568 | ||
|
|
f77d5db38e | ||
|
|
fed2d11905 | ||
|
|
8861a76bd0 | ||
|
|
56f0b2628e | ||
|
|
7a6cc84d41 | ||
|
|
a34c99385c | ||
|
|
94339c569e | ||
|
|
1df06efd97 | ||
|
|
52e52e83a9 | ||
|
|
9f092dbe9f | ||
|
|
b324a02fbb | ||
|
|
5fa4cfee04 | ||
|
|
5e48c1ac51 | ||
|
|
e0d12ba4d0 | ||
|
|
f7f3b22786 | ||
|
|
e43697d359 | ||
|
|
72057adc94 | ||
|
|
2f9a144023 | ||
|
|
9e2ecd67f9 | ||
|
|
b195f519e2 | ||
|
|
9f346e7ba0 | ||
|
|
41695e601d | ||
|
|
066859cca0 | ||
|
|
44f9059338 | ||
|
|
d95d00b7c8 | ||
|
|
880d061164 | ||
|
|
bb5f1dae75 | ||
|
|
1bb1b69dea | ||
|
|
3562c2d1d1 | ||
|
|
46736a737f | ||
|
|
5b1e860489 | ||
|
|
8e095335be | ||
|
|
7fa94843aa | ||
|
|
be7fd6bfb4 | ||
|
|
90d1873ad0 | ||
|
|
341fb6d8f3 | ||
|
|
ca577f7aaa | ||
|
|
2f359e201d | ||
|
|
3c1af2acf0 | ||
|
|
7cb66f7fd3 | ||
|
|
a48966f08c | ||
|
|
ecf07bd128 | ||
|
|
1c613ada6f | ||
|
|
4250ac821f | ||
|
|
aefbe21b46 | ||
|
|
808dbfcade | ||
|
|
2896e3666c | ||
|
|
24a68a79df | ||
|
|
fe5b88522e | ||
|
|
5f7e06ba6f | ||
|
|
d791fa7460 | ||
|
|
01888c98bc | ||
|
|
581af4eced | ||
|
|
7ef4dbcaf6 | ||
|
|
4e5bc82a17 | ||
|
|
5c9ce575f6 | ||
|
|
ee140369a3 | ||
|
|
db1f7fcf63 | ||
|
|
eccd5b4c5d | ||
|
|
d721de13aa | ||
|
|
3c9640df27 |
6
.github/workflows/release_notes.yml
vendored
6
.github/workflows/release_notes.yml
vendored
@@ -29,7 +29,11 @@ jobs:
|
||||
steps:
|
||||
- name: Update notes
|
||||
run: |
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG \
|
||||
| jq -r '.body' \
|
||||
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
|
||||
| sed -E 's/by @mergify //'
|
||||
)
|
||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
||||
|
||||
|
||||
@@ -59,12 +59,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
|
||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
"label": "Is Group"
|
||||
},
|
||||
{
|
||||
"fetch_from": "parent_account.company",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
|
||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
||||
"General Ledger",
|
||||
"Balance Sheet",
|
||||
"Profit and Loss Statement",
|
||||
"Cash Flow Statement",
|
||||
"Cash Flow",
|
||||
"Accounts Payable",
|
||||
"Accounts Receivable",
|
||||
]) {
|
||||
|
||||
@@ -1525,7 +1525,8 @@
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
"Clients cr\u00e9diteurs": {
|
||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
||||
"account_number": "4191"
|
||||
"account_number": "4191",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||
"account_number": "4196"
|
||||
@@ -3141,4 +3142,4 @@
|
||||
"account_number": "7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,23 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
enable_immutable_ledger: function (frm) {
|
||||
if (!frm.doc.enable_immutable_ledger) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = __("Enabling this will change the way how cancelled transactions are handled.");
|
||||
msg += " ";
|
||||
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||
msg += "<br>";
|
||||
msg += "Do you still want to enable immutable ledger?";
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
() => {},
|
||||
() => {
|
||||
frm.set_value("enable_immutable_ledger", 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"enable_immutable_ledger",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
@@ -105,7 +106,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
|
||||
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
|
||||
"fieldname": "check_supplier_invoice_uniqueness",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@@ -454,6 +455,13 @@
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well",
|
||||
"fieldname": "enable_immutable_ledger",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Immutable Ledger"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -461,7 +469,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.568638",
|
||||
"modified": "2024-05-11 23:19:44.673975",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -490,4 +498,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class AccountsSettings(Document):
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
|
||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||
if (!frm.doc.bank_account) {
|
||||
frappe.msgprint(__("Please select Bank Account"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
@@ -118,7 +119,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.922473",
|
||||
"modified": "2024-04-28 14:40:50.910884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
|
||||
@@ -495,12 +495,12 @@ def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
|
||||
@@ -540,14 +540,14 @@ def get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
exact_match=None,
|
||||
common_filters=None,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
exact_match=None,
|
||||
account_from_to=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
common_filters=None,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
@@ -719,7 +719,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -148,7 +148,19 @@ class Dunning(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = [
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -127,6 +127,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -194,6 +194,7 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
@@ -225,6 +226,7 @@ class JournalEntry(AccountsController):
|
||||
self.unlink_inter_company_jv()
|
||||
self.unlink_asset_adjustment_entry()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
@@ -442,6 +444,28 @@ class JournalEntry(AccountsController):
|
||||
if status:
|
||||
inv_disc_doc.set_status(status=status)
|
||||
|
||||
def update_booked_depreciation(self):
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
|
||||
total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
fb_row.db_set(
|
||||
"total_number_of_booked_depreciations", total_number_of_booked_depreciations
|
||||
)
|
||||
break
|
||||
|
||||
def unlink_advance_entry_reference(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
@@ -457,7 +481,7 @@ class JournalEntry(AccountsController):
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
@@ -1034,6 +1058,17 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"party",
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -751,6 +752,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -766,6 +768,16 @@
|
||||
"label": "In Words",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "company.reconcile_on_advance_payment_date",
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Reconcile on Advance Payment Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -779,7 +791,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:25:07.366347",
|
||||
"modified": "2024-05-17 10:21:11.199445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -197,6 +197,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
if self.party_type not in ("Customer", "Supplier"):
|
||||
return
|
||||
|
||||
@@ -1225,88 +1226,71 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
amount = self.base_paid_amount
|
||||
else:
|
||||
amount = self.base_received_amount
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
amount_in_account_currency = amount * exchange_rate
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: amount,
|
||||
dr_or_cr + "_in_account_currency": amount_in_account_currency,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
else:
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(
|
||||
d.reference_doctype, d.reference_name, "cost_center"
|
||||
)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
||||
d
|
||||
)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1321,7 +1305,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
||||
"""
|
||||
If 'entry' is passed, GL enties only for that reference is added.
|
||||
If 'entry' is passed, GL entries only for that reference is added.
|
||||
"""
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
references = [x for x in self.get("references")]
|
||||
@@ -1333,8 +1317,6 @@ class PaymentEntry(AccountsController):
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Payment Entry",
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
@@ -1362,13 +1344,16 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
if self.reconcile_on_advance_payment_date:
|
||||
posting_date = self.posting_date
|
||||
else:
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
|
||||
@@ -1477,6 +1477,68 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_advance_as_liability_against_order(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": advance_account,
|
||||
},
|
||||
)
|
||||
|
||||
po = create_purchase_order(supplier="_Test Supplier")
|
||||
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
|
||||
pe.save().submit()
|
||||
|
||||
pre_reconciliation_gle = [
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.expected_gle = pre_reconciliation_gle
|
||||
self.check_gl_entries()
|
||||
|
||||
# Make Purchase Invoice against the order
|
||||
pi = _make_purchase_invoice(po.name)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": pe.doctype,
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 5000,
|
||||
"allocated_amount": 5000,
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
|
||||
# # assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
|
||||
def check_pl_entries(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
pl_entries = (
|
||||
|
||||
@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
if (typeof r.message === "string") {
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
this.frm.refresh();
|
||||
},
|
||||
|
||||
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_date(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 1500
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.posting_date = add_days(nowdate(), -1)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
pi = self.create_purchase_invoice(qty=10, rate=100)
|
||||
self.assertNotEqual(pe.posting_date, pi.posting_date)
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -13,7 +14,12 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||
|
||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||
PAYMENT_URL = "https://example.com/payment"
|
||||
|
||||
payment_gateways = [
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"},
|
||||
]
|
||||
|
||||
payment_method = [
|
||||
{
|
||||
@@ -29,13 +35,21 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"currency": "USD",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway Phone",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Phone",
|
||||
"currency": "USD",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestPaymentRequest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
for payment_gateway in payment_gateways:
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
|
||||
for method in payment_method:
|
||||
if not frappe.db.get_value(
|
||||
@@ -45,6 +59,25 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
send_email = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
|
||||
return_value=None,
|
||||
)
|
||||
self.send_email = send_email.start()
|
||||
self.addCleanup(send_email.stop)
|
||||
get_payment_url = patch(
|
||||
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
return_value=PAYMENT_URL,
|
||||
)
|
||||
self.get_payment_url = get_payment_url.start()
|
||||
self.addCleanup(get_payment_url.stop)
|
||||
_get_payment_gateway_controller = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
|
||||
)
|
||||
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
|
||||
self.addCleanup(_get_payment_gateway_controller.stop)
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
so_inr.disable_rounded_total = 1
|
||||
@@ -75,6 +108,83 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
self.assertEqual(pr.reference_name, si_usd.name)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
|
||||
def test_payment_channels(self):
|
||||
so = make_sales_order(currency="USD")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=False,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.flags.mute_email = True # but temporarily prohibit sending
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertIsNone(pr.payment_url)
|
||||
self.assertEqual(self.send_email.call_count, 0) # hence: no increment
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
|
||||
pr.cancel()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Phone - USD",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Phone")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertIsNone(pr.payment_url)
|
||||
self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
|
||||
pr.cancel()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||
self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
|
||||
pr.cancel()
|
||||
|
||||
so = make_sales_order(currency="USD", do_not_save=True)
|
||||
# no-op; for optical consistency with how a webshop SO would look like
|
||||
so.order_type = "Shopping Cart"
|
||||
so.save()
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
order_type="Shopping Cart",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertIsNone(pr.payment_url)
|
||||
self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
|
||||
pr.cancel()
|
||||
|
||||
def test_payment_entry_against_purchase_invoice(self):
|
||||
si_usd = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
|
||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
|
||||
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -74,15 +74,21 @@
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_13",
|
||||
"threshold_percentage",
|
||||
"priority",
|
||||
"dynamic_condition_tab",
|
||||
"condition",
|
||||
"column_break_66",
|
||||
"section_break_13",
|
||||
"apply_multiple_pricing_rules",
|
||||
"apply_discount_on_rate",
|
||||
"column_break_66",
|
||||
"threshold_percentage",
|
||||
"validate_pricing_rule_section",
|
||||
"validate_applied_rule",
|
||||
"column_break_texp",
|
||||
"rule_description",
|
||||
"priority_section",
|
||||
"has_priority",
|
||||
"column_break_sayg",
|
||||
"priority",
|
||||
"help_section",
|
||||
"pricing_rule_help",
|
||||
"reference_section",
|
||||
@@ -477,7 +483,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
@@ -487,6 +493,7 @@
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_priority",
|
||||
"description": "Higher the number, higher the priority",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@@ -513,6 +520,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -525,7 +533,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Help Article",
|
||||
"options": "Simple"
|
||||
},
|
||||
{
|
||||
@@ -603,12 +612,42 @@
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_condition_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dynamic Condition"
|
||||
},
|
||||
{
|
||||
"fieldname": "validate_pricing_rule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validate Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_texp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sayg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:17.521896",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
|
||||
PricingRuleItemCode,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||
PricingRuleItemGroup,
|
||||
)
|
||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
||||
free_item_rate: DF.Currency
|
||||
free_item_uom: DF.Link | None
|
||||
free_qty: DF.Float
|
||||
has_priority: DF.Check
|
||||
is_cumulative: DF.Check
|
||||
is_recursive: DF.Check
|
||||
item_groups: DF.Table[PricingRuleItemGroup]
|
||||
@@ -156,6 +155,12 @@ class PricingRule(Document):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.has_priority and not self.priority:
|
||||
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||
|
||||
if self.priority and not self.has_priority:
|
||||
self.has_priority = 1
|
||||
|
||||
for apply_on, field in apply_on_dict.items():
|
||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||
@@ -573,6 +578,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
||||
# Apply discount on discounted rate
|
||||
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
||||
elif args.price_list_rate:
|
||||
value = pricing_rule.get(field, 0)
|
||||
calculate_discount_percentage = False
|
||||
if field == "discount_percentage":
|
||||
field = "discount_amount"
|
||||
value = args.price_list_rate * (value / 100)
|
||||
calculate_discount_percentage = True
|
||||
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
|
||||
item_details[field] += value if pricing_rule else args.get(field, 0)
|
||||
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
|
||||
item_details.discount_percentage = flt(
|
||||
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
|
||||
)
|
||||
else:
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
|
||||
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_pricing_rule_for_transaction_with_condition(self):
|
||||
make_item("PR Transaction Condition")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
make_pricing_rule(
|
||||
selling=1,
|
||||
min_qty=0,
|
||||
price_or_product_discount="Product",
|
||||
apply_on="Transaction",
|
||||
free_item="PR Transaction Condition",
|
||||
free_qty=1,
|
||||
free_item_rate=10,
|
||||
condition="customer=='_Test Customer 1'",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
|
||||
self.assertEqual(len(si.items), 2)
|
||||
self.assertEqual(si.items[1].rate, 10)
|
||||
|
||||
si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
|
||||
self.assertEqual(len(si1.items), 1)
|
||||
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
@@ -1102,7 +1126,116 @@ class TestPricingRule(unittest.TestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 4)
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Amount",
|
||||
"discount_amount": 100,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_amount, 200)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_priority_of_multiple_pricing_rules(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"has_priority": 1,
|
||||
"priority": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"has_priority": 1,
|
||||
"priority": 3,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_percentage, 20)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
@@ -1132,6 +1265,7 @@ def make_pricing_rule(**args):
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
@@ -32,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
||||
|
||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and pricing_rules[0].has_priority:
|
||||
continue
|
||||
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
break
|
||||
|
||||
@@ -560,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
|
||||
if pricing_rules:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
||||
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
|
||||
|
||||
if not pricing_rules:
|
||||
remove_free_item(doc)
|
||||
@@ -653,7 +658,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = round(qty)
|
||||
qty = math.floor(qty)
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -502,7 +504,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
|
||||
ageing_filters = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"report_date": doc.to_date,
|
||||
"report_date": doc.posting_date,
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
|
||||
@@ -340,10 +340,11 @@
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">30 Days</th>
|
||||
<th style="width: 25%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 25%">120 Days</th>
|
||||
<th style="width: 25%">0 - 30 Days</th>
|
||||
<th style="width: 25%">30 - 60 Days</th>
|
||||
<th style="width: 25%">60 - 90 Days</th>
|
||||
<th style="width: 25%">90 - 120 Days</th>
|
||||
<th style="width: 20%">Above 120 Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -352,6 +353,7 @@
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
{
|
||||
"fieldname": "cost_center_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:23.244686",
|
||||
"modified": "2024-05-03 17:16:51.666461",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "PSOA Cost Center",
|
||||
|
||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
cost_center_name: DF.Link | None
|
||||
cost_center_name: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -489,10 +489,12 @@ function hide_fields(doc) {
|
||||
|
||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
if (cur_frm.fields_dict["items"]) {
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
}
|
||||
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -708,6 +708,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1063,7 +1064,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if item.get("purchase_receipt"):
|
||||
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
@@ -1147,7 +1148,7 @@ class PurchaseInvoice(BuyingController):
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
@@ -1175,6 +1176,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
@@ -1191,7 +1193,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
@@ -1207,7 +1212,7 @@ class PurchaseInvoice(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,11 +53,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.013542",
|
||||
"modified": "2024-05-23 17:00:42.984798",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,7 +98,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.740806",
|
||||
"modified": "2024-05-23 17:00:31.540640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
|
||||
@@ -2042,7 +2042,7 @@
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
@@ -2203,7 +2203,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:30:26.272441",
|
||||
"modified": "2024-05-08 18:02:28.549041",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -393,6 +393,9 @@ class SalesInvoice(SellingController):
|
||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||
|
||||
if not tax_withholding_details:
|
||||
@@ -452,6 +455,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
@@ -1783,6 +1784,49 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_gle_in_transaction_currency(self):
|
||||
# create multi currency sales invoice with 2 items with same income account
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_submit=True,
|
||||
)
|
||||
# add 2nd item with same income account
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 80,
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select transaction_currency, transaction_exchange_rate,
|
||||
debit_in_transaction_currency, credit_in_transaction_currency
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
expected_gle = {
|
||||
"transaction_currency": "USD",
|
||||
"transaction_exchange_rate": 50,
|
||||
"debit_in_transaction_currency": 0,
|
||||
"credit_in_transaction_currency": 180,
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
for field in expected_gle:
|
||||
self.assertEqual(expected_gle[field], gle[field])
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
@@ -3690,9 +3734,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
|
||||
@@ -870,7 +870,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -926,7 +927,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.139679",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
||||
|
||||
# Process subscription and create first invoice
|
||||
# Subscription status will be unpaid since due date has already passed
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-04-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multi_currency_subscription(self):
|
||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
@@ -20,6 +21,7 @@
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
@@ -28,7 +30,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:52.419915",
|
||||
"modified": "2024-04-30 10:26:48.218294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Account",
|
||||
|
||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
||||
if d.get("account") in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||
|
||||
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||
existing_accounts.append(d.get("account"))
|
||||
|
||||
def validate_thresholds(self):
|
||||
@@ -282,6 +285,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if taxable_vouchers:
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
||||
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
||||
# rest of the below logic to function properly
|
||||
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
||||
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
||||
if tax_deducted_on_advances:
|
||||
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
@@ -418,7 +429,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
@@ -443,6 +454,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
return sum(entries)
|
||||
|
||||
|
||||
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||
"""
|
||||
Only applies for Taxes deducted on Advance Payments
|
||||
"""
|
||||
advance_tax_from_across_fiscal_year = sum(
|
||||
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
||||
)
|
||||
return advance_tax_from_across_fiscal_year
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils import today
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||
|
||||
|
||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
# create relevant supplier, etc
|
||||
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
make_pan_no_field()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_cumulative_threshold_tds(self):
|
||||
frappe.db.set_value(
|
||||
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
|
||||
def add_company_to_fy(fy, company):
|
||||
if not [x.company for x in fy.companies if x.company == company]:
|
||||
fy.append("companies", {"company": company})
|
||||
fy.save()
|
||||
|
||||
# setup previous fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company=test_company)
|
||||
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
|
||||
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
|
||||
add_company_to_fy(self.prev_fy, test_company)
|
||||
else:
|
||||
# make previous fiscal year
|
||||
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
|
||||
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
|
||||
self.prev_fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year_start_date": start,
|
||||
"year_end_date": end,
|
||||
"companies": [{"company": test_company}],
|
||||
}
|
||||
)
|
||||
self.prev_fy.save()
|
||||
|
||||
# setup tax withholding category for previous fiscal year
|
||||
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||
cat.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": self.prev_fy.year_start_date,
|
||||
"to_date": self.prev_fy.year_end_date,
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000,
|
||||
},
|
||||
)
|
||||
cat.save()
|
||||
|
||||
def test_tds_across_fiscal_year(self):
|
||||
"""
|
||||
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||
--||-----FY 2023-----||-----FY 2024-----||--
|
||||
--||-----Advance-----||---Inv1---Inv2---||--
|
||||
"""
|
||||
self.set_previous_fy_and_tax_category()
|
||||
supplier = "Test TDS Supplier"
|
||||
# Cumulative threshold 30000 and tax rate 10%
|
||||
category = "Cumulative Threshold TDS"
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
supplier,
|
||||
{
|
||||
"tax_withholding_category": category,
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
|
||||
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
|
||||
po.transaction_date = po_and_advance_posting_date
|
||||
po.taxes = []
|
||||
po.apply_tds = False
|
||||
po.tax_withholding_category = None
|
||||
po.save().submit()
|
||||
|
||||
# Partial advance
|
||||
payment = get_payment_entry(po.doctype, po.name)
|
||||
payment.posting_date = po_and_advance_posting_date
|
||||
payment.paid_amount = 60000
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = category
|
||||
payment.references = []
|
||||
payment.taxes = []
|
||||
payment.save().submit()
|
||||
|
||||
self.assertEqual(len(payment.taxes), 1)
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
|
||||
# Multiple partial invoices
|
||||
payment.reload()
|
||||
pi1 = make_purchase_invoice(source_name=po.name)
|
||||
pi1.apply_tds = True
|
||||
pi1.tax_withholding_category = category
|
||||
pi1.items[0].qty = 3
|
||||
pi1.items[0].rate = 10000
|
||||
advances = pi1.get_advance_entries()
|
||||
pi1.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi1.save().submit()
|
||||
pi1.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
|
||||
|
||||
pi2 = make_purchase_invoice(source_name=po.name)
|
||||
pi2.apply_tds = True
|
||||
pi2.tax_withholding_category = category
|
||||
pi2.items[0].qty = 3
|
||||
pi2.items[0].rate = 10000
|
||||
advances = pi2.get_advance_entries()
|
||||
pi2.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi2.save().submit()
|
||||
pi2.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi2.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -163,7 +165,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||
from frappe.utils.dashboard import cache_source
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -240,10 +241,16 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||
entry.debit_in_account_currency
|
||||
)
|
||||
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||
entry.debit_in_transaction_currency
|
||||
)
|
||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||
entry.credit_in_account_currency
|
||||
)
|
||||
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||
entry.credit_in_transaction_currency
|
||||
)
|
||||
else:
|
||||
merged_gl_map.append(entry)
|
||||
|
||||
@@ -574,6 +581,8 @@ def make_reverse_gl_entries(
|
||||
and make reverse gl entries by swapping debit and credit
|
||||
"""
|
||||
|
||||
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
||||
|
||||
if not gl_entries:
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
@@ -605,7 +614,6 @@ def make_reverse_gl_entries(
|
||||
for x in gl_entries:
|
||||
query = (
|
||||
frappe.qb.update(gle)
|
||||
.set(gle.is_cancelled, True)
|
||||
.set(gle.modified, now())
|
||||
.set(gle.modified_by, frappe.session.user)
|
||||
.where(
|
||||
@@ -620,9 +628,14 @@ def make_reverse_gl_entries(
|
||||
& (gle.voucher_detail_no == x.voucher_detail_no)
|
||||
)
|
||||
)
|
||||
|
||||
if not immutable_ledger_enabled:
|
||||
query = query.set(gle.is_cancelled, True)
|
||||
|
||||
query.run()
|
||||
else:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
@@ -641,6 +654,10 @@ def make_reverse_gl_entries(
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
if immutable_ledger_enabled:
|
||||
new_gle["is_cancelled"] = 0
|
||||
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||
|
||||
if new_gle["debit"] or new_gle["credit"]:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
@@ -733,3 +750,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
|
||||
def is_immutable_ledger_enabled():
|
||||
return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||
|
||||
@@ -188,7 +188,9 @@ def set_address_details(
|
||||
*,
|
||||
ignore_permissions=False,
|
||||
):
|
||||
billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
|
||||
)
|
||||
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
||||
|
||||
if filters.account_type:
|
||||
conditions["account_type"] = filters.account_type
|
||||
return conditions
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
@@ -501,8 +501,9 @@ class ReceivablePayableReport:
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
# If no or single payment terms, no need to split the row
|
||||
if len(payment_terms_details) <= 1:
|
||||
# If single payment terms, no need to split the row
|
||||
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||
return
|
||||
|
||||
for d in payment_terms_details:
|
||||
@@ -1027,20 +1028,6 @@ class ReceivablePayableReport:
|
||||
fieldtype="Link",
|
||||
options="Contact",
|
||||
)
|
||||
if self.filters.party_type == "Customer":
|
||||
self.add_column(
|
||||
_("Customer Name"),
|
||||
fieldname="customer_name",
|
||||
fieldtype="Link",
|
||||
options="Customer",
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
_("Supplier Name"),
|
||||
fieldname="supplier_name",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
|
||||
@@ -266,6 +266,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
filters.end_date = fiscal_year.year_end_date
|
||||
|
||||
for company in companies:
|
||||
filters.company = company
|
||||
amount = get_account_type_based_gl_data(company, filters)
|
||||
|
||||
if amount and account_type == "Depreciation":
|
||||
|
||||
@@ -58,9 +58,9 @@ class Deferred_Item:
|
||||
For a given GL/Journal posting, get balance based on item type
|
||||
"""
|
||||
if self.type == "Deferred Sale Item":
|
||||
return entry.debit - entry.credit
|
||||
return flt(entry.debit) - flt(entry.credit)
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
return -(entry.credit - entry.debit)
|
||||
return -(flt(entry.credit) - flt(entry.debit))
|
||||
return 0
|
||||
|
||||
def get_item_total(self):
|
||||
@@ -147,7 +147,7 @@ class Deferred_Item:
|
||||
actual = 0
|
||||
for posting in self.gle_entries:
|
||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
|
||||
period_sum += self.get_amount(posting)
|
||||
if posting.posted == "posted":
|
||||
actual += self.get_amount(posting)
|
||||
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
qb.from_(inv_item)
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.join(gle)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
|
||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_zero_amount(self):
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 12
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=3910,
|
||||
price_list_rate=3910,
|
||||
warehouse=self.warehouse,
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-12-30"
|
||||
pi.items[0].service_end_date = "2022-12-30"
|
||||
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||
pi.items[0].expense_account = self.expense_account
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2022-01-01",
|
||||
end_date="2022-01-31",
|
||||
type="Expense",
|
||||
company=self.company,
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2022-01-01",
|
||||
"period_end_date": "2022-01-31",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
|
||||
# fetch the invoice from deferred invoices list
|
||||
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||
# make sure the list isn't empty
|
||||
self.assertTrue(inv)
|
||||
# calculate the total deferred expense for the period
|
||||
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||
# make sure the total deferred expense is greater than 0
|
||||
self.assertLess(deferred_exp, 0)
|
||||
|
||||
@@ -219,7 +219,8 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
if filters.account:
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
return list(set(all_accounts)) if all_accounts else None
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
@@ -460,7 +461,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
for gle in gl_entries:
|
||||
group_by_value = gle.get(group_by)
|
||||
gle.voucher_type = gle.voucher_type
|
||||
gle.voucher_subtype = _(gle.voucher_subtype)
|
||||
gle.against_voucher_type = _(gle.against_voucher_type)
|
||||
gle.remarks = _(gle.remarks)
|
||||
|
||||
@@ -720,20 +720,22 @@ class GrossProfitGenerator:
|
||||
frappe.qb.from_(purchase_invoice_item)
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
)
|
||||
|
||||
if row.project:
|
||||
query.where(purchase_invoice_item.project == row.project)
|
||||
query = query.where(purchase_invoice_item.project == row.project)
|
||||
|
||||
if row.cost_center:
|
||||
query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||
|
||||
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
|
||||
query.limit(1)
|
||||
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
|
||||
last_purchase_rate = query.run()
|
||||
|
||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
||||
|
||||
if d.reference_type == "Purchase Invoice":
|
||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
||||
else:
|
||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
||||
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||
payment_amount = d.amount
|
||||
|
||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||
|
||||
if d.against_voucher:
|
||||
if d.against_voucher_no:
|
||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||
|
||||
row = [
|
||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
||||
d.party_type,
|
||||
d.party,
|
||||
d.posting_date,
|
||||
d.against_voucher,
|
||||
d.against_voucher_no,
|
||||
invoice.posting_date,
|
||||
invoice.due_date,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.amount,
|
||||
d.remarks,
|
||||
d.age,
|
||||
d.range1,
|
||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = []
|
||||
|
||||
if not filters.party_type:
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
filters.party_type = "Supplier"
|
||||
else:
|
||||
filters.party_type = "Customer"
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("party_type=%(party_type)s")
|
||||
conditions.append(ple.delinked.eq(0))
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
conditions.append(ple.party_type.eq("Supplier"))
|
||||
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||
else:
|
||||
conditions.append(ple.party_type.eq("Customer"))
|
||||
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||
|
||||
if filters.party:
|
||||
conditions.append("party=%(party)s")
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("against_voucher_type=%(reference_type)s")
|
||||
filters["reference_type"] = (
|
||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
||||
)
|
||||
conditions.append(ple.party.eq(filters.party))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append("posting_date <= %(to_date)s")
|
||||
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||
|
||||
return "and " + " and ".join(conditions) if conditions else ""
|
||||
if filters.get("company"):
|
||||
conditions.append(ple.company.eq(filters.get("company")))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
||||
""".format(get_conditions(filters)),
|
||||
filters,
|
||||
as_dict=1,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
Abs(ple.amount).as_("amount"),
|
||||
ple.remarks,
|
||||
ple.against_voucher_no,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
|
||||
|
||||
def get_invoice_posting_date_map(filters):
|
||||
invoice_details = {}
|
||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
||||
dt = (
|
||||
qb.DocType("Sales Invoice")
|
||||
if filters.get("payment_type") == _("Incoming")
|
||||
else qb.DocType("Purchase Invoice")
|
||||
)
|
||||
res = (
|
||||
qb.from_(dt)
|
||||
.select(dt.name, dt.posting_date, dt.due_date)
|
||||
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||
.run(as_dict=1)
|
||||
)
|
||||
for t in res:
|
||||
invoice_details[t.name] = t
|
||||
|
||||
return invoice_details
|
||||
|
||||
@@ -56,7 +56,7 @@ def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||
):
|
||||
if isinstance(boolean, str):
|
||||
boolean = frappe.json.loads(boolean)
|
||||
boolean = loads(boolean)
|
||||
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
||||
@@ -516,6 +516,10 @@ def reconcile_against_document(
|
||||
doc.make_advance_gl_entries()
|
||||
else:
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||
|
||||
process_debit_credit_difference(gl_map)
|
||||
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
||||
|
||||
# Only update outstanding for newly linked vouchers
|
||||
@@ -1105,7 +1109,7 @@ def get_companies():
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent, company, is_root=False, include_disabled=False):
|
||||
if isinstance(include_disabled, str):
|
||||
include_disabled = frappe.json.loads(include_disabled)
|
||||
include_disabled = loads(include_disabled)
|
||||
from erpnext.accounts.report.financial_statements import sort_accounts
|
||||
|
||||
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
|
||||
|
||||
@@ -652,7 +652,7 @@ frappe.ui.form.on("Asset", {
|
||||
);
|
||||
|
||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||
frm.set_value("purchase_receipt_amount", purchase_amount);
|
||||
frm.set_value("purchase_amount", purchase_amount);
|
||||
frm.set_value("asset_quantity", asset_quantity);
|
||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||
if (item.asset_location) {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"is_fully_depreciated",
|
||||
"section_break_36",
|
||||
"finance_books",
|
||||
@@ -72,7 +72,7 @@
|
||||
"status",
|
||||
"booked_fixed_asset",
|
||||
"column_break_51",
|
||||
"purchase_receipt_amount",
|
||||
"purchase_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from",
|
||||
@@ -257,12 +257,6 @@
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of Depreciations Booked"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
|
||||
@@ -408,15 +402,6 @@
|
||||
"options": "Purchase Receipt",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Amount",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||
"fieldname": "purchase_invoice",
|
||||
@@ -546,6 +531,21 @@
|
||||
"label": "Additional Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Amount",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -589,7 +589,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:06:32.494326",
|
||||
"modified": "2024-05-21 13:46:21.066483",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -628,7 +628,7 @@
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
|
||||
@@ -89,13 +89,13 @@ class Asset(AccountsController):
|
||||
maintenance_required: DF.Check
|
||||
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
||||
next_depreciation_date: DF.Date | None
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
policy_number: DF.Data | None
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_amount: DF.Currency
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -145,7 +145,7 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
|
||||
self.set_total_booked_depreciations()
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
self.status = self.get_status()
|
||||
|
||||
@@ -356,7 +356,7 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
|
||||
error_message = _(
|
||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
)
|
||||
@@ -419,7 +419,7 @@ class Asset(AccountsController):
|
||||
|
||||
if not self.is_existing_asset:
|
||||
self.opening_accumulated_depreciation = 0
|
||||
self.number_of_depreciations_booked = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
else:
|
||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
@@ -430,15 +430,15 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
if not self.number_of_depreciations_booked:
|
||||
frappe.throw(_("Please set Number of Depreciations Booked"))
|
||||
if not self.opening_number_of_booked_depreciations:
|
||||
frappe.throw(_("Please set Opening Number of Booked Depreciations"))
|
||||
else:
|
||||
self.number_of_depreciations_booked = 0
|
||||
self.opening_number_of_booked_depreciations = 0
|
||||
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked"
|
||||
"Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
|
||||
).format(row.idx),
|
||||
title=_("Invalid Schedule"),
|
||||
)
|
||||
@@ -459,6 +459,17 @@ class Asset(AccountsController):
|
||||
).format(row.idx)
|
||||
)
|
||||
|
||||
def set_total_booked_depreciations(self):
|
||||
# set value of total number of booked depreciations field
|
||||
for fb_row in self.get("finance_books"):
|
||||
total_number_of_booked_depreciations = self.opening_number_of_booked_depreciations
|
||||
depr_schedule = get_depr_schedule(self.name, "Active", fb_row.finance_book)
|
||||
if depr_schedule:
|
||||
for je in depr_schedule:
|
||||
if je.journal_entry:
|
||||
total_number_of_booked_depreciations += 1
|
||||
fb_row.db_set("total_number_of_booked_depreciations", total_number_of_booked_depreciations)
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
@@ -695,11 +706,7 @@ class Asset(AccountsController):
|
||||
purchase_document = self.get_purchase_document()
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if (
|
||||
purchase_document
|
||||
and self.purchase_receipt_amount
|
||||
and getdate(self.available_for_use_date) <= getdate()
|
||||
):
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -707,8 +714,8 @@ class Asset(AccountsController):
|
||||
"against": fixed_asset_account,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"posting_date": self.available_for_use_date,
|
||||
"credit": self.purchase_receipt_amount,
|
||||
"credit_in_account_currency": self.purchase_receipt_amount,
|
||||
"credit": self.purchase_amount,
|
||||
"credit_in_account_currency": self.purchase_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -722,8 +729,8 @@ class Asset(AccountsController):
|
||||
"against": cwip_account,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"posting_date": self.available_for_use_date,
|
||||
"debit": self.purchase_receipt_amount,
|
||||
"debit_in_account_currency": self.purchase_receipt_amount,
|
||||
"debit": self.purchase_amount,
|
||||
"debit_in_account_currency": self.purchase_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -1119,8 +1126,8 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
)
|
||||
|
||||
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||
if asset.purchase_receipt_amount:
|
||||
new_asset.purchase_receipt_amount = new_gross_purchase_amount
|
||||
if asset.purchase_amount:
|
||||
new_asset.purchase_amount = new_gross_purchase_amount
|
||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||
new_asset.asset_quantity = split_qty
|
||||
new_asset.split_from = asset.name
|
||||
|
||||
@@ -323,6 +323,7 @@ def _make_journal_entry_for_depreciation(
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
asset.reload()
|
||||
idx = cint(asset_depr_schedule_doc.finance_book_id)
|
||||
row = asset.get("finance_books")[idx - 1]
|
||||
row.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||
|
||||
@@ -355,7 +355,7 @@ class TestAsset(AssetSetup):
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
@@ -453,7 +453,7 @@ class TestAsset(AssetSetup):
|
||||
purchase_date="2020-01-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=6,
|
||||
number_of_depreciations_booked=1,
|
||||
opening_number_of_booked_depreciations=1,
|
||||
frequency_of_depreciation=10,
|
||||
depreciation_start_date="2021-01-01",
|
||||
opening_accumulated_depreciation=20000,
|
||||
@@ -739,7 +739,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2030-06-06",
|
||||
is_existing_asset=1,
|
||||
number_of_depreciations_booked=2,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=47095.89,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2032-12-31",
|
||||
@@ -789,7 +789,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2030-01-01",
|
||||
is_existing_asset=1,
|
||||
depreciation_method="Double Declining Balance",
|
||||
number_of_depreciations_booked=1,
|
||||
opening_number_of_booked_depreciations=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2031-12-31",
|
||||
@@ -1000,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
||||
)
|
||||
self.assertEqual(depreciation_amount, 30000)
|
||||
@@ -1123,8 +1123,8 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_number_of_depreciations_booked(self):
|
||||
"""Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is."""
|
||||
def test_opening_booked_depreciations(self):
|
||||
"""Tests if an error is raised when opening_number_of_booked_depreciations is not specified when opening_accumulated_depreciation is."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1140,9 +1140,9 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_number_of_depreciations(self):
|
||||
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
|
||||
"""Tests if an error is raised when opening_number_of_booked_depreciations >= total_number_of_depreciations."""
|
||||
|
||||
# number_of_depreciations_booked > total_number_of_depreciations
|
||||
# opening_number_of_booked_depreciations > total_number_of_depreciations
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
@@ -1151,13 +1151,13 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2020-07-01",
|
||||
opening_accumulated_depreciation=10000,
|
||||
number_of_depreciations_booked=5,
|
||||
opening_number_of_booked_depreciations=5,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
# number_of_depreciations_booked = total_number_of_depreciations
|
||||
# opening_number_of_booked_depreciations = total_number_of_depreciations
|
||||
asset_2 = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
@@ -1166,7 +1166,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2020-07-01",
|
||||
opening_accumulated_depreciation=10000,
|
||||
number_of_depreciations_booked=5,
|
||||
opening_number_of_booked_depreciations=5,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
@@ -1502,7 +1502,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
asset = create_asset(calculate_depreciation=1)
|
||||
asset.opening_accumulated_depreciation = 2000
|
||||
asset.number_of_depreciations_booked = 1
|
||||
asset.opening_number_of_booked_depreciations = 1
|
||||
|
||||
asset.finance_books[0].expected_value_after_useful_life = 100
|
||||
asset.save()
|
||||
@@ -1696,9 +1696,9 @@ def create_asset(**args):
|
||||
"purchase_date": args.purchase_date or "2015-01-01",
|
||||
"calculate_depreciation": args.calculate_depreciation or 0,
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||
"opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
|
||||
"purchase_amount": args.purchase_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||
@@ -1723,6 +1723,7 @@ def create_asset(**args):
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||
"shift_based": args.shift_based or 0,
|
||||
"rate_of_depreciation": args.rate_of_depreciation or 0,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1738,12 +1739,12 @@ def create_asset(**args):
|
||||
return asset
|
||||
|
||||
|
||||
def create_asset_category():
|
||||
def create_asset_category(enable_cwip=1):
|
||||
asset_category = frappe.new_doc("Asset Category")
|
||||
asset_category.asset_category_name = "Computers"
|
||||
asset_category.total_number_of_depreciations = 3
|
||||
asset_category.frequency_of_depreciation = 3
|
||||
asset_category.enable_cwip_accounting = 1
|
||||
asset_category.enable_cwip_accounting = enable_cwip
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
|
||||
@@ -616,8 +616,7 @@ class AssetCapitalization(StockController):
|
||||
asset_doc.available_for_use_date = self.posting_date
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||
@@ -653,7 +652,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
@@ -526,7 +526,7 @@ def create_depreciation_asset(**args):
|
||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||
|
||||
asset.gross_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_receipt_amount = asset.gross_purchase_amount
|
||||
asset.purchase_amount = asset.gross_purchase_amount
|
||||
|
||||
finance_book = asset.append("finance_books")
|
||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"finance_book",
|
||||
"finance_book_id",
|
||||
"depreciation_details_section",
|
||||
@@ -171,10 +171,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Number of Depreciations Booked",
|
||||
"label": "Opening Number of Booked Depreciations",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ class AssetDepreciationSchedule(Document):
|
||||
gross_purchase_amount: DF.Currency
|
||||
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
||||
notes: DF.SmallText | None
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
rate_of_depreciation: DF.Percent
|
||||
shift_based: DF.Check
|
||||
@@ -161,7 +161,7 @@ class AssetDepreciationSchedule(Document):
|
||||
return (
|
||||
asset_doc.gross_purchase_amount != self.gross_purchase_amount
|
||||
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
|
||||
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
|
||||
or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
|
||||
@@ -194,7 +194,7 @@ class AssetDepreciationSchedule(Document):
|
||||
self.finance_book = row.finance_book
|
||||
self.finance_book_id = row.idx
|
||||
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
|
||||
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
|
||||
self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
|
||||
self.gross_purchase_amount = asset_doc.gross_purchase_amount
|
||||
self.depreciation_method = row.depreciation_method
|
||||
self.total_number_of_depreciations = row.total_number_of_depreciations
|
||||
@@ -263,7 +263,7 @@ class AssetDepreciationSchedule(Document):
|
||||
row.db_update()
|
||||
|
||||
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
self.number_of_depreciations_booked
|
||||
self.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
has_pro_rata = _check_is_pro_rata(asset_doc, row)
|
||||
@@ -285,6 +285,7 @@ class AssetDepreciationSchedule(Document):
|
||||
number_of_pending_depreciations = final_number_of_depreciations - start
|
||||
yearly_opening_wdv = value_after_depreciation
|
||||
current_fiscal_year_end_date = None
|
||||
prev_per_day_depr = True
|
||||
for n in range(start, final_number_of_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
|
||||
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
||||
else:
|
||||
prev_depreciation_amount = 0
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||
self,
|
||||
asset_doc,
|
||||
value_after_depreciation,
|
||||
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
number_of_pending_depreciations,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
if not has_pro_rata or (
|
||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||
@@ -327,7 +328,7 @@ class AssetDepreciationSchedule(Document):
|
||||
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
||||
from_date = add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
@@ -362,18 +363,31 @@ class AssetDepreciationSchedule(Document):
|
||||
row.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
|
||||
).format(
|
||||
frappe.bold(asset_doc.gross_purchase_amount),
|
||||
frappe.bold(row.total_number_of_depreciations),
|
||||
frappe.bold(row.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
|
||||
(
|
||||
(self.opening_number_of_booked_depreciations - 1)
|
||||
* row.frequency_of_depreciation
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(asset_doc.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
(self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
@@ -389,7 +403,8 @@ class AssetDepreciationSchedule(Document):
|
||||
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
|
||||
asset_doc.to_date = add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
|
||||
(n + self.opening_number_of_booked_depreciations)
|
||||
* cint(row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
@@ -535,7 +550,7 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
has_pro_rata = False
|
||||
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
@@ -556,12 +571,12 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
|
||||
if wdv_or_dd_non_yearly:
|
||||
return add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(asset_doc.number_of_depreciations_booked * 12),
|
||||
(asset_doc.opening_number_of_booked_depreciations * 12),
|
||||
)
|
||||
else:
|
||||
return add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
|
||||
@@ -599,11 +614,12 @@ def get_depreciation_amount(
|
||||
prev_depreciation_amount=0,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
number_of_pending_depreciations=0,
|
||||
prev_per_day_depr=0,
|
||||
):
|
||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(
|
||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||
)
|
||||
), None
|
||||
else:
|
||||
return get_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
@@ -614,6 +630,7 @@ def get_depreciation_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
@@ -637,49 +654,14 @@ def get_straight_line_or_manual_depr_amount(
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_prorata_based:
|
||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset,
|
||||
row,
|
||||
schedule_idx,
|
||||
number_of_pending_depreciations,
|
||||
amount,
|
||||
)
|
||||
from_date = add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
@@ -692,46 +674,32 @@ def get_straight_line_or_manual_depr_amount(
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
from_date = add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
|
||||
|
||||
def get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
):
|
||||
total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
|
||||
from_date, total_depreciable_days = _get_total_days(
|
||||
row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
|
||||
)
|
||||
return daily_depr_amount * total_depreciable_days
|
||||
|
||||
|
||||
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
@@ -740,7 +708,7 @@ def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
shift = (
|
||||
@@ -779,6 +747,7 @@ def get_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
return get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
@@ -788,6 +757,7 @@ def get_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
@@ -799,6 +769,39 @@ def get_default_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
|
||||
return _get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
), None
|
||||
else:
|
||||
return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
def _get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
):
|
||||
if cint(fb_row.frequency_of_depreciation) == 12:
|
||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||
@@ -825,6 +828,75 @@ def get_default_wdv_or_dd_depr_amount(
|
||||
return prev_depreciation_amount
|
||||
|
||||
|
||||
def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
|
||||
if schedule_idx == 0:
|
||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
|
||||
|
||||
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
|
||||
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||
else:
|
||||
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||
else:
|
||||
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
|
||||
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||
else:
|
||||
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||
|
||||
|
||||
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
|
||||
""" "
|
||||
Returns monthly depreciation amount when year changes
|
||||
1. Calculate per day depr based on new year
|
||||
2. Calculate monthly amount based on new per day amount
|
||||
"""
|
||||
from_date, days_in_month = _get_total_days(
|
||||
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||
)
|
||||
per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
|
||||
return (per_day_depr * days_in_month), per_day_depr
|
||||
|
||||
|
||||
def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
|
||||
""" "
|
||||
Returns monthly depreciation amount based on prev per day depr
|
||||
Calculate per day depr only for the first month
|
||||
"""
|
||||
from_date, days_in_month = _get_total_days(
|
||||
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||
)
|
||||
return (prev_per_day_depr * days_in_month), prev_per_day_depr
|
||||
|
||||
|
||||
def get_per_day_depr(
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
from_date,
|
||||
):
|
||||
to_date = add_days(add_years(from_date, 1), -1)
|
||||
total_days = date_diff(to_date, from_date) + 1
|
||||
per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
|
||||
return per_day_depr
|
||||
|
||||
|
||||
def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
|
||||
from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
|
||||
to_date = add_months(from_date, frequency_of_depreciation)
|
||||
if is_last_day_of_the_month(depreciation_start_date):
|
||||
to_date = get_last_day(to_date)
|
||||
from_date = add_days(get_last_day(from_date), 1)
|
||||
return from_date, date_diff(to_date, from_date) + 1
|
||||
|
||||
|
||||
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||
asset_depr_schedules_names = []
|
||||
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
post_depreciation_entries,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depr_schedule,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,3 +30,168 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||
|
||||
def test_daily_prorata_based_depr_on_sl_method(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-01-01",
|
||||
depreciation_start_date="2020-01-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=24,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-01-31", 4234.97, 4234.97],
|
||||
["2020-02-29", 3961.75, 8196.72],
|
||||
["2020-03-31", 4234.97, 12431.69],
|
||||
["2020-04-30", 4098.36, 16530.05],
|
||||
["2020-05-31", 4234.97, 20765.02],
|
||||
["2020-06-30", 4098.36, 24863.38],
|
||||
["2020-07-31", 4234.97, 29098.35],
|
||||
["2020-08-31", 4234.97, 33333.32],
|
||||
["2020-09-30", 4098.36, 37431.68],
|
||||
["2020-10-31", 4234.97, 41666.65],
|
||||
["2020-11-30", 4098.36, 45765.01],
|
||||
["2020-12-31", 4234.97, 49999.98],
|
||||
["2021-01-31", 4246.58, 54246.56],
|
||||
["2021-02-28", 3835.62, 58082.18],
|
||||
["2021-03-31", 4246.58, 62328.76],
|
||||
["2021-04-30", 4109.59, 66438.35],
|
||||
["2021-05-31", 4246.58, 70684.93],
|
||||
["2021-06-30", 4109.59, 74794.52],
|
||||
["2021-07-31", 4246.58, 79041.1],
|
||||
["2021-08-31", 4246.58, 83287.68],
|
||||
["2021-09-30", 4109.59, 87397.27],
|
||||
["2021-10-31", 4246.58, 91643.85],
|
||||
["2021-11-30", 4109.59, 95753.44],
|
||||
["2021-12-31", 4246.56, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Test for Written Down Value Method
|
||||
# Frequency of deprciation = 3
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2021-02-20",
|
||||
depreciation_start_date="2021-03-31",
|
||||
frequency_of_depreciation=3,
|
||||
total_number_of_depreciations=6,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2021-03-31", 4383.56, 4383.56],
|
||||
["2021-06-30", 9535.45, 13919.01],
|
||||
["2021-09-30", 9640.23, 23559.24],
|
||||
["2021-12-31", 9640.23, 33199.47],
|
||||
["2022-03-31", 9430.66, 42630.13],
|
||||
["2022-06-30", 5721.27, 48351.4],
|
||||
["2022-08-20", 51648.6, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Frequency of deprciation = 6
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_6_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-02-20",
|
||||
depreciation_start_date="2020-02-29",
|
||||
frequency_of_depreciation=6,
|
||||
total_number_of_depreciations=6,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-02-29", 1092.90, 1092.90],
|
||||
["2020-08-31", 19944.01, 21036.91],
|
||||
["2021-02-28", 19618.83, 40655.74],
|
||||
["2021-08-31", 11966.4, 52622.14],
|
||||
["2022-02-28", 11771.3, 64393.44],
|
||||
["2022-08-31", 7179.84, 71573.28],
|
||||
["2023-02-20", 28426.72, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Frequency of deprciation = 12
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_12_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-02-20",
|
||||
depreciation_start_date="2020-03-31",
|
||||
frequency_of_depreciation=12,
|
||||
total_number_of_depreciations=4,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-03-31", 4480.87, 4480.87],
|
||||
["2021-03-31", 38207.65, 42688.52],
|
||||
["2022-03-31", 22924.59, 65613.11],
|
||||
["2023-03-31", 13754.76, 79367.87],
|
||||
["2024-02-20", 20632.13, 100000],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_update_total_number_of_booked_depreciations(self):
|
||||
# check if updates total number of booked depreciations when depreciation gets booked
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
opening_accumulated_depreciation=2000,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2020-03-01",
|
||||
depreciation_start_date="2020-03-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=24,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date="2021-03-31")
|
||||
asset.reload()
|
||||
"""
|
||||
opening_number_of_booked_depreciations = 2
|
||||
number_of_booked_depreciations till 2021-03-31 = 13
|
||||
total_number_of_booked_depreciations = 15
|
||||
"""
|
||||
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 15)
|
||||
|
||||
# cancel depreciation entry
|
||||
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
|
||||
|
||||
frappe.get_doc("Journal Entry", depr_entry).cancel()
|
||||
asset.reload()
|
||||
|
||||
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"finance_book",
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"total_number_of_booked_depreciations",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_5",
|
||||
@@ -104,12 +105,19 @@
|
||||
"fieldname": "shift_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Number of Booked Depreciations ",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:34.342264",
|
||||
"modified": "2024-05-21 15:48:20.907250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -28,6 +28,7 @@ class AssetFinanceBook(Document):
|
||||
rate_of_depreciation: DF.Percent
|
||||
salvage_value_percentage: DF.Percent
|
||||
shift_based: DF.Check
|
||||
total_number_of_booked_depreciations: DF.Int
|
||||
total_number_of_depreciations: DF.Int
|
||||
value_after_depreciation: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -377,7 +377,7 @@ class AssetRepair(AccountsController):
|
||||
def calculate_last_schedule_date(self, asset, row, extra_months):
|
||||
asset.flags.increase_in_asset_life = True
|
||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
asset.number_of_depreciations_booked
|
||||
asset.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||
@@ -410,7 +410,7 @@ class AssetRepair(AccountsController):
|
||||
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
|
||||
asset.flags.increase_in_asset_life = True
|
||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
asset.number_of_depreciations_booked
|
||||
asset.opening_number_of_booked_depreciations
|
||||
)
|
||||
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||
|
||||
@@ -159,8 +159,9 @@ def prepare_chart_data(data, filters):
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
date_field = "purchase_date"
|
||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filtered_data = [d for d in data if not d.get(date_field)]
|
||||
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
else:
|
||||
filters_filter_based_on = filters.filter_based_on
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
|
||||
@@ -369,7 +369,7 @@ class PurchaseOrder(BuyingController):
|
||||
item.idx, item.fg_item
|
||||
)
|
||||
)
|
||||
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
|
||||
elif not item.bom and not frappe.get_value("Item", item.fg_item, "default_bom"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Default BOM not found for FG Item {1}").format(
|
||||
item.idx, item.fg_item
|
||||
@@ -919,6 +919,14 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.job_card = source_doc.items[idx].job_card
|
||||
if not target_doc.supplier_warehouse:
|
||||
# WIP warehouse is set as Supplier Warehouse in Job Card
|
||||
target_doc.supplier_warehouse = frappe.get_cached_value(
|
||||
"Job Card", item.job_card, "wip_warehouse"
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
|
||||
@@ -772,12 +772,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
account = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
account = frappe.get_doc("Account", {"account_name": account_name, "company": company})
|
||||
|
||||
return account
|
||||
|
||||
@@ -808,22 +803,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(po_doc.name)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": pe.doctype,
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 5000,
|
||||
"allocated_amount": 5000,
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
pe.reload()
|
||||
po_doc.reload()
|
||||
self.assertEqual(po_doc.advance_paid, 0)
|
||||
|
||||
company_doc.book_advance_payments_in_separate_party_account = False
|
||||
company_doc.save()
|
||||
|
||||
|
||||
@@ -110,7 +110,9 @@
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"page_break"
|
||||
"page_break",
|
||||
"column_break_pjyo",
|
||||
"job_card"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -909,13 +911,24 @@
|
||||
{
|
||||
"fieldname": "column_break_fyqr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pjyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_card",
|
||||
"fieldtype": "Link",
|
||||
"label": "Job Card",
|
||||
"options": "Job Card",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:24.979325",
|
||||
"modified": "2024-03-27 13:12:24.979325",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||
args: {
|
||||
doctype: "Supplier",
|
||||
tag: args.tag,
|
||||
tag: "%" + args.tag + "%",
|
||||
},
|
||||
callback: load_suppliers,
|
||||
});
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_tab",
|
||||
@@ -485,7 +485,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:10:48.412732",
|
||||
"modified": "2024-05-08 18:02:57.342931",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
frappe.query_reports["Purchase Order Trends"]["filters"].push({
|
||||
fieldname: "include_closed_orders",
|
||||
label: __("Include Closed Orders"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
});
|
||||
|
||||
@@ -778,6 +778,9 @@ class AccountsController(TransactionBase):
|
||||
# reset pricing rule fields if pricing_rule_removed
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||
item.expense_account = value
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||
"is_fixed_asset"
|
||||
):
|
||||
@@ -1939,7 +1942,7 @@ class AccountsController(TransactionBase):
|
||||
def set_advance_payment_status(self):
|
||||
new_status = None
|
||||
|
||||
stati = frappe.get_list(
|
||||
stati = frappe.get_all(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": self.doctype,
|
||||
@@ -2208,10 +2211,10 @@ class AccountsController(TransactionBase):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
|
||||
@@ -712,6 +712,7 @@ class BuyingController(SubcontractingController):
|
||||
def auto_make_assets(self, asset_items):
|
||||
items_data = get_asset_item_details(asset_items)
|
||||
messages = []
|
||||
alert = False
|
||||
|
||||
for d in self.items:
|
||||
if d.is_fixed_asset:
|
||||
@@ -761,9 +762,10 @@ class BuyingController(SubcontractingController):
|
||||
frappe.bold(d.item_code)
|
||||
)
|
||||
)
|
||||
alert = True
|
||||
|
||||
for message in messages:
|
||||
frappe.msgprint(message, title="Success", indicator="green")
|
||||
frappe.msgprint(message, title="Success", indicator="green", alert=alert)
|
||||
|
||||
def make_asset(self, row, is_grouped_asset=False):
|
||||
if not row.asset_location:
|
||||
@@ -787,7 +789,7 @@ class BuyingController(SubcontractingController):
|
||||
"supplier": self.supplier,
|
||||
"purchase_date": self.posting_date,
|
||||
"calculate_depreciation": 0,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
|
||||
@@ -427,6 +427,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
& (stock_ledger_entry.batch_no.isnotnull())
|
||||
)
|
||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||
.having(Sum(stock_ledger_entry.actual_qty) > 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
@@ -477,6 +478,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||
)
|
||||
.groupby(bundle.batch_no, bundle.warehouse)
|
||||
.having(Sum(bundle.qty) > 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if source_doc.item_code:
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
and not source_doc.use_serial_batch_fields
|
||||
):
|
||||
target_doc.set("use_serial_batch_fields", 1)
|
||||
|
||||
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not available_dict:
|
||||
frappe.throw(_("No Serial / Batches are available for return"))
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
for row in batch_serial_nos:
|
||||
key = row.voucher_detail_no
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
del filters["serial_and_batch_bundle"]
|
||||
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||
pluck_field = "rejected_serial_and_batch_bundle"
|
||||
|
||||
_bundle_ids = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
if not is_rejected:
|
||||
_bundle_ids.extend(
|
||||
frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
for serial_no, sn_qty in data.serial_nos.items():
|
||||
if sn_qty != 0:
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
else:
|
||||
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||
|
||||
elif data.batches:
|
||||
for batch_no, batch_qty in data.batches.items():
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
batch_qty = batch_qty * -1
|
||||
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
warehouse,
|
||||
)
|
||||
|
||||
if batch_qty <= 0:
|
||||
frappe.throw(
|
||||
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||
title=_("Batch Not Available for Return"),
|
||||
)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
if batch_qty > qty:
|
||||
filterd_serial_batch["batches"][batch_no] = qty
|
||||
qty = 0
|
||||
else:
|
||||
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||
qty -= batch_qty
|
||||
|
||||
return filterd_serial_batch
|
||||
|
||||
|
||||
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
return get_batch_qty(
|
||||
batch_no,
|
||||
warehouse,
|
||||
posting_date=parent_doc.posting_date,
|
||||
posting_time=parent_doc.posting_time,
|
||||
for_stock_levels=True,
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"item_code": child_doc.item_code,
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.qty,
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
return cls_obj.name
|
||||
|
||||
|
||||
def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
available_serial_batch_for_return,
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
@@ -205,6 +210,7 @@ class StockController(AccountsController):
|
||||
"company": self.company,
|
||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||
}
|
||||
|
||||
@@ -216,6 +222,125 @@ class StockController(AccountsController):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
self.make_bundle_for_non_rejected_qty(table_name)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
self.make_bundle_for_rejected_qty(table_name)
|
||||
|
||||
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||
field, reference_ids = self.get_reference_ids(
|
||||
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(
|
||||
field, child_doctype, reference_ids, is_rejected=True
|
||||
)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
qty_field = "rejected_qty"
|
||||
warehouse_field = "rejected_warehouse"
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||
field, reference_ids = self.get_reference_ids(table_name)
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Purchase Receipt": "purchase_receipt_item",
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"POS Invoice": "pos_invoice_item",
|
||||
}.get(self.doctype)
|
||||
|
||||
if not bundle_field:
|
||||
bundle_field = "serial_and_batch_bundle"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
reference_ids = []
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not self.is_serial_batch_item(row.item_code):
|
||||
continue
|
||||
|
||||
if (
|
||||
row.get(field)
|
||||
and (
|
||||
qty_field == "qty"
|
||||
and not row.get("return_qty_from_rejected_warehouse")
|
||||
or qty_field == "rejected_qty"
|
||||
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||
)
|
||||
and not row.get("use_serial_batch_fields")
|
||||
and not row.get(bundle_field)
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -610,35 +735,16 @@ class StockController(AccountsController):
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
):
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
docstatus=self.docstatus,
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
@@ -1227,8 +1333,8 @@ def get_accounting_ledger_preview(doc, filters):
|
||||
"debit",
|
||||
"credit",
|
||||
"against",
|
||||
"party",
|
||||
"party_type",
|
||||
"party",
|
||||
"cost_center",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
@@ -1404,7 +1510,12 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
@@ -1551,3 +1662,38 @@ def create_item_wise_repost_entries(
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
return repost_entries
|
||||
|
||||
|
||||
def make_bundle_for_material_transfer(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||
|
||||
if not kwargs.type_of_transaction:
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
|
||||
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.serial_no:
|
||||
if row.serial_no and not consumed_bundles.serial_nos:
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.batch_no:
|
||||
if row.batch_no and not consumed_bundles.batch_nos:
|
||||
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def get_available_materials(self):
|
||||
|
||||
@@ -74,8 +74,10 @@ def get_data(filters, conditions):
|
||||
|
||||
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
||||
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||
cond += " and t1.status != 'Closed'"
|
||||
|
||||
if not filters.get("include_closed_orders"):
|
||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||
cond += " and t1.status != 'Closed'"
|
||||
|
||||
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
||||
cond += " and t1.quotation_to = 'Customer'"
|
||||
|
||||
@@ -121,7 +121,7 @@ def send_mail(entry, email_campaign):
|
||||
doctype="Email Campaign",
|
||||
name=email_campaign.name,
|
||||
subject=frappe.render_template(email_template.get("subject"), context),
|
||||
content=frappe.render_template(email_template.get("response"), context),
|
||||
content=frappe.render_template(email_template.response_, context),
|
||||
sender=sender,
|
||||
recipients=recipient_list,
|
||||
communication_medium="Email",
|
||||
|
||||
3226
erpnext/locale/ar.po
3226
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
5895
erpnext/locale/bs.po
5895
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
3664
erpnext/locale/de.po
3664
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
3232
erpnext/locale/eo.po
3232
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
3736
erpnext/locale/es.po
3736
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
4837
erpnext/locale/fa.po
4837
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3228
erpnext/locale/fr.po
3228
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
9704
erpnext/locale/tr.po
9704
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("bom_no", "operations", function (doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.controllers.queries.bom",
|
||||
filters: {
|
||||
currency: frm.doc.currency,
|
||||
company: frm.doc.company,
|
||||
item: row.finished_good,
|
||||
is_active: 1,
|
||||
docstatus: 1,
|
||||
track_semi_finished_goods: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("source_warehouse", "items", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -85,6 +100,27 @@ frappe.ui.form.on("BOM", {
|
||||
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
||||
},
|
||||
|
||||
default_source_warehouse(frm) {
|
||||
if (frm.doc.default_source_warehouse) {
|
||||
frm.doc.operations.forEach((d) => {
|
||||
frappe.model.set_value(
|
||||
d.doctype,
|
||||
d.name,
|
||||
"source_warehouse",
|
||||
frm.doc.default_source_warehouse
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
default_target_warehouse(frm) {
|
||||
if (frm.doc.default_source_warehouse) {
|
||||
frm.doc.operations.forEach((d) => {
|
||||
frappe.model.set_value(d.doctype, d.name, "fg_warehouse", frm.doc.default_target_warehouse);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
frm.toggle_enable("item", frm.doc.__islocal);
|
||||
|
||||
@@ -96,22 +132,35 @@ frappe.ui.form.on("BOM", {
|
||||
});
|
||||
|
||||
if (!frm.is_new() && frm.doc.docstatus < 2) {
|
||||
frm.add_custom_button(__("Update Cost"), function () {
|
||||
frm.events.update_cost(frm, true);
|
||||
});
|
||||
frm.add_custom_button(__("Browse BOM"), function () {
|
||||
frappe.route_options = {
|
||||
bom: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("Tree", "BOM");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Update Cost"),
|
||||
function () {
|
||||
frm.events.update_cost(frm, true);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Browse BOM"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
bom: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("Tree", "BOM");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
||||
frm.add_custom_button(__("New Version"), function () {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("New Version"),
|
||||
function () {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
@@ -432,6 +481,28 @@ frappe.ui.form.on("BOM", {
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("BOM Operation", {
|
||||
bom_no(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
|
||||
if (row.bom_no && row.finished_good) {
|
||||
frappe.call({
|
||||
method: "add_materials_from_bom",
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
finished_good: row.finished_good,
|
||||
bom_no: row.bom_no,
|
||||
operation_row_id: row.idx,
|
||||
qty: row.finished_good_qty,
|
||||
},
|
||||
callback(r) {
|
||||
refresh_field("items");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
|
||||
conversion_rate(doc) {
|
||||
if (this.frm.doc.currency === this.get_company_currency()) {
|
||||
@@ -801,3 +872,88 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
|
||||
__("Set Quantity")
|
||||
);
|
||||
}
|
||||
|
||||
frappe.ui.form.on("BOM Operation", {
|
||||
add_raw_materials(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
frm.events._prompt_for_raw_materials(frm, row);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("BOM", {
|
||||
_prompt_for_raw_materials(frm, row) {
|
||||
let fields = frm.events.get_fields_for_prompt(frm, row);
|
||||
frm._bom_rm_dialog = new frappe.ui.Dialog({
|
||||
title: __("Add Raw Materials"),
|
||||
fields: fields,
|
||||
primary_action_label: __("Add"),
|
||||
primary_action: () => {
|
||||
let values = frm._bom_rm_dialog.get_values();
|
||||
if (values) {
|
||||
frm.events._add_raw_materials(frm, values);
|
||||
frm._bom_rm_dialog.hide();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frm._bom_rm_dialog.show();
|
||||
},
|
||||
|
||||
get_fields_for_prompt(frm, row) {
|
||||
return [
|
||||
{
|
||||
label: __("Raw Materials"),
|
||||
fieldname: "items",
|
||||
fieldtype: "Table",
|
||||
reqd: 1,
|
||||
fields: [
|
||||
{
|
||||
label: __("Item"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
change() {
|
||||
let doc = this.doc;
|
||||
doc.qty = 1.0;
|
||||
this.grid.set_value("qty", 1.0, doc);
|
||||
},
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
name: ["!=", row.finished_good],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Qty"),
|
||||
fieldname: "qty",
|
||||
default: 1.0,
|
||||
fieldtype: "Float",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldname: "operation_row_id",
|
||||
fieldtype: "Data",
|
||||
hidden: 1,
|
||||
default: row.idx,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
_add_raw_materials(frm, values) {
|
||||
frm.call({
|
||||
method: "add_raw_materials",
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
operation_row_id: values.operation_row_id,
|
||||
items: values.items,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,19 +26,22 @@
|
||||
"column_break_ivyw",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"materials_section",
|
||||
"items",
|
||||
"section_break_21",
|
||||
"operations_section_section",
|
||||
"with_operations",
|
||||
"track_semi_finished_goods",
|
||||
"column_break_23",
|
||||
"transfer_material_against",
|
||||
"routing",
|
||||
"fg_based_operating_cost",
|
||||
"column_break_joxb",
|
||||
"default_source_warehouse",
|
||||
"default_target_warehouse",
|
||||
"fg_based_section_section",
|
||||
"operating_cost_per_bom_quantity",
|
||||
"operations_section",
|
||||
"operations",
|
||||
"materials_section",
|
||||
"items",
|
||||
"scrap_section",
|
||||
"scrap_items_section",
|
||||
"scrap_items",
|
||||
@@ -59,8 +62,8 @@
|
||||
"base_total_cost",
|
||||
"more_info_tab",
|
||||
"item_name",
|
||||
"description",
|
||||
"column_break_27",
|
||||
"description",
|
||||
"has_variants",
|
||||
"quality_inspection_section_break",
|
||||
"inspection_required",
|
||||
@@ -211,7 +214,7 @@
|
||||
},
|
||||
{
|
||||
"default": "Work Order",
|
||||
"depends_on": "with_operations",
|
||||
"depends_on": "eval: doc.with_operations === 1 && doc.track_semi_finished_goods === 0",
|
||||
"fieldname": "transfer_material_against",
|
||||
"fieldtype": "Select",
|
||||
"label": "Transfer Material Against",
|
||||
@@ -406,8 +409,8 @@
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "section_break0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Materials Required (Exploded)"
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Exploded Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "exploded_items",
|
||||
@@ -485,11 +488,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Operations"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Operations"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -534,6 +532,8 @@
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.with_operations",
|
||||
"fieldname": "operations_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operations"
|
||||
@@ -617,7 +617,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "BOM Creator",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_creator_item",
|
||||
@@ -625,11 +626,36 @@
|
||||
"label": "BOM Creator Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oxbz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "with_operations",
|
||||
"description": "Users can consume raw materials and add semi-finished goods or final finished goods against the operation using job cards.",
|
||||
"fieldname": "track_semi_finished_goods",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Semi Finished Goods"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_joxb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Source Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
@@ -637,7 +663,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified": "2024-04-02 16:24:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -10,7 +10,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, cstr, flt, today
|
||||
from frappe.utils import cint, cstr, flt, parse_json, today
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
import erpnext
|
||||
@@ -125,6 +125,8 @@ class BOM(WebsiteGenerator):
|
||||
company: DF.Link
|
||||
conversion_rate: DF.Float
|
||||
currency: DF.Link
|
||||
default_source_warehouse: DF.Link | None
|
||||
default_target_warehouse: DF.Link | None
|
||||
description: DF.SmallText | None
|
||||
exploded_items: DF.Table[BOMExplosionItem]
|
||||
fg_based_operating_cost: DF.Check
|
||||
@@ -136,6 +138,7 @@ class BOM(WebsiteGenerator):
|
||||
item: DF.Link
|
||||
item_name: DF.Data | None
|
||||
items: DF.Table[BOMItem]
|
||||
track_semi_finished_goods: DF.Check
|
||||
operating_cost: DF.Currency
|
||||
operating_cost_per_bom_quantity: DF.Currency
|
||||
operations: DF.Table[BOMOperation]
|
||||
@@ -245,6 +248,7 @@ class BOM(WebsiteGenerator):
|
||||
self.clear_inspection()
|
||||
self.validate_main_item()
|
||||
self.validate_currency()
|
||||
self.set_materials_based_on_operation_bom()
|
||||
self.set_conversion_rate()
|
||||
self.set_plc_conversion_rate()
|
||||
self.validate_uom_is_interger()
|
||||
@@ -544,6 +548,9 @@ class BOM(WebsiteGenerator):
|
||||
if not self.with_operations:
|
||||
self.set("operations", [])
|
||||
|
||||
if not self.with_operations and self.track_semi_finished_goods:
|
||||
self.track_semi_finished_goods = 0
|
||||
|
||||
def clear_inspection(self):
|
||||
if not self.inspection_required:
|
||||
self.quality_inspection_template = None
|
||||
@@ -645,6 +652,49 @@ class BOM(WebsiteGenerator):
|
||||
if self.name in {d.bom_no for d in self.items}:
|
||||
_throw_error(self.name)
|
||||
|
||||
def set_materials_based_on_operation_bom(self):
|
||||
if not self.track_semi_finished_goods:
|
||||
return
|
||||
|
||||
for row in self.get("operations"):
|
||||
if row.bom_no and row.finished_good:
|
||||
self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty)
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_raw_materials(self, operation_row_id, items):
|
||||
if isinstance(items, str):
|
||||
items = parse_json(items)
|
||||
|
||||
for row in items:
|
||||
row = parse_json(row)
|
||||
|
||||
row.update(get_item_details(row.get("item_code")))
|
||||
row.operation_row_id = operation_row_id
|
||||
row.idx = None
|
||||
row.name = None
|
||||
self.append("items", row)
|
||||
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None):
|
||||
if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}):
|
||||
frappe.throw(_("BOM {0} not found for the item {1}").format(bom_no, finished_good))
|
||||
|
||||
if not qty:
|
||||
qty = 1
|
||||
|
||||
for row in self.items:
|
||||
if row.operation_row_id == operation_row_id:
|
||||
return
|
||||
|
||||
bom_items = get_bom_items(bom_no, self.company, qty=qty, fetch_exploded=0)
|
||||
for row in bom_items:
|
||||
row.uom = row.stock_uom
|
||||
row.operation_row_id = operation_row_id
|
||||
row.idx = None
|
||||
self.append("items", row)
|
||||
|
||||
def traverse_tree(self, bom_list=None):
|
||||
def _get_children(bom_no):
|
||||
children = frappe.cache().hget("bom_children", bom_no)
|
||||
@@ -1094,6 +1144,11 @@ def get_bom_items_as_dict(
|
||||
):
|
||||
item_dict = {}
|
||||
|
||||
group_by_cond = "group by item_code, stock_uom"
|
||||
if frappe.get_cached_value("BOM", bom, "track_semi_finished_goods"):
|
||||
fetch_exploded = 0
|
||||
group_by_cond = "group by item_code, operation_row_id, stock_uom"
|
||||
|
||||
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
|
||||
query = """select
|
||||
bom_item.item_code,
|
||||
@@ -1122,7 +1177,7 @@ def get_bom_items_as_dict(
|
||||
and bom.name = %(bom)s
|
||||
and item.is_stock_item in (1, {is_stock_item})
|
||||
{where_conditions}
|
||||
group by item_code, stock_uom
|
||||
{group_by_cond}
|
||||
order by idx"""
|
||||
|
||||
is_stock_item = 0 if include_non_stock_items else 1
|
||||
@@ -1132,6 +1187,7 @@ def get_bom_items_as_dict(
|
||||
where_conditions="",
|
||||
is_stock_item=is_stock_item,
|
||||
qty_field="stock_qty",
|
||||
group_by_cond=group_by_cond,
|
||||
select_columns=""", bom_item.source_warehouse, bom_item.operation,
|
||||
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
|
||||
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
|
||||
@@ -1147,6 +1203,7 @@ def get_bom_items_as_dict(
|
||||
select_columns=", item.description",
|
||||
is_stock_item=is_stock_item,
|
||||
qty_field="stock_qty",
|
||||
group_by_cond=group_by_cond,
|
||||
)
|
||||
|
||||
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
||||
@@ -1158,15 +1215,20 @@ def get_bom_items_as_dict(
|
||||
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
|
||||
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
|
||||
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
|
||||
bom_item.description, bom_item.base_rate as rate """,
|
||||
bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """,
|
||||
group_by_cond=group_by_cond,
|
||||
)
|
||||
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
|
||||
|
||||
for item in items:
|
||||
if item.item_code in item_dict:
|
||||
item_dict[item.item_code]["qty"] += flt(item.qty)
|
||||
key = item.item_code
|
||||
if item.operation_row_id:
|
||||
key = (item.item_code, item.operation_row_id)
|
||||
|
||||
if key in item_dict:
|
||||
item_dict[key]["qty"] += flt(item.qty)
|
||||
else:
|
||||
item_dict[item.item_code] = item
|
||||
item_dict[key] = item
|
||||
|
||||
for item, item_details in item_dict.items():
|
||||
for d in [
|
||||
|
||||
@@ -88,9 +88,77 @@ frappe.ui.form.on("BOM Creator", {
|
||||
reqd: 1,
|
||||
default: 1.0,
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Track Operations"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "track_operations",
|
||||
onchange: (r) => {
|
||||
let track_operations = dialog.get_value("track_operations");
|
||||
if (r.type === "input" && !track_operations) {
|
||||
dialog.set_value("track_semi_finished_goods", 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Track Semi Finished Goods"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "track_semi_finished_goods",
|
||||
depends_on: "eval:doc.track_operations",
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Final Product Operation"),
|
||||
depends_on: "eval:doc.track_semi_finished_goods",
|
||||
},
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldtype: "Link",
|
||||
fieldname: "operation",
|
||||
options: "Operation",
|
||||
default: "Assembly",
|
||||
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
|
||||
depends_on: "eval:doc.track_semi_finished_goods",
|
||||
},
|
||||
{
|
||||
label: __("Operation Time (in mins)"),
|
||||
fieldtype: "Float",
|
||||
fieldname: "operation_time",
|
||||
mandatory_depends_on: "eval:doc.track_semi_finished_goods",
|
||||
depends_on: "eval:doc.track_semi_finished_goods",
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Workstation Type"),
|
||||
fieldtype: "Link",
|
||||
fieldname: "workstation_type",
|
||||
options: "Workstation",
|
||||
depends_on: "eval:doc.track_semi_finished_goods",
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldtype: "Link",
|
||||
fieldname: "workstation",
|
||||
options: "Workstation",
|
||||
depends_on: "eval:doc.track_semi_finished_goods",
|
||||
get_query() {
|
||||
let workstation_type = dialog.get_value("workstation_type");
|
||||
|
||||
if (workstation_type) {
|
||||
return {
|
||||
filters: {
|
||||
workstation_type: dialog.get_value("workstation_type"),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Create"),
|
||||
primary_action: (values) => {
|
||||
frm.events.validate_dialog_values(frm, values);
|
||||
|
||||
values.doctype = frm.doc.doctype;
|
||||
frappe.db.insert(values).then((doc) => {
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
@@ -102,6 +170,18 @@ frappe.ui.form.on("BOM Creator", {
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
validate_dialog_values(frm, values) {
|
||||
if (values.track_semi_finished_goods) {
|
||||
if (values.final_operation_time <= 0) {
|
||||
frappe.throw(__("Operation Time must be greater than 0"));
|
||||
}
|
||||
|
||||
if (!values.workstation && !values.workstation_type) {
|
||||
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
set_queries(frm) {
|
||||
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
@@ -121,6 +201,16 @@ frappe.ui.form.on("BOM Creator", {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("workstation", (doc) => {
|
||||
if (doc.workstation_type) {
|
||||
return {
|
||||
filters: {
|
||||
workstation_type: doc.workstation_type,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
|
||||
@@ -37,6 +37,23 @@
|
||||
"items",
|
||||
"costing_detail",
|
||||
"raw_material_cost",
|
||||
"configuration_section",
|
||||
"track_operations",
|
||||
"column_break_obzr",
|
||||
"track_semi_finished_goods",
|
||||
"final_product_operation_section",
|
||||
"operation",
|
||||
"operation_time",
|
||||
"column_break_xnlu",
|
||||
"workstation_type",
|
||||
"workstation",
|
||||
"final_product_warehouse_section",
|
||||
"skip_material_transfer",
|
||||
"backflush_from_wip_warehouse",
|
||||
"source_warehouse",
|
||||
"column_break_buha",
|
||||
"wip_warehouse",
|
||||
"fg_warehouse",
|
||||
"remarks_tab",
|
||||
"remarks",
|
||||
"section_break_yixm",
|
||||
@@ -278,6 +295,104 @@
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Log",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "configuration_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "track_operations",
|
||||
"fieldname": "track_semi_finished_goods",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Semi Finished Goods"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_obzr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "track_operations",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Operations"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.track_semi_finished_goods === 1",
|
||||
"fieldname": "final_product_operation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Final Product Operation & Workstation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xnlu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Operation",
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_time",
|
||||
"fieldtype": "Float",
|
||||
"label": "Operation Time (in mins)"
|
||||
},
|
||||
{
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Workstation",
|
||||
"options": "Workstation"
|
||||
},
|
||||
{
|
||||
"fieldname": "workstation_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Workstation Type",
|
||||
"options": "Workstation Type"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.backflush_from_wip_warehouse",
|
||||
"fieldname": "source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work In Progress Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finished Good Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.track_semi_finished_goods === 1",
|
||||
"fieldname": "final_product_warehouse_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Final Product Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "skip_material_transfer",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Material Transfer"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.skip_material_transfer",
|
||||
"fieldname": "backflush_from_wip_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backflush Materials From WIP Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_buha",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
@@ -288,7 +403,7 @@
|
||||
"link_fieldname": "bom_creator"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-02 16:30:59.779190",
|
||||
"modified": "2024-05-26 15:47:10.101420",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator",
|
||||
|
||||
@@ -43,16 +43,20 @@ class BOMCreator(Document):
|
||||
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
|
||||
|
||||
amended_from: DF.Link | None
|
||||
backflush_from_wip_warehouse: DF.Check
|
||||
buying_price_list: DF.Link | None
|
||||
company: DF.Link
|
||||
conversion_rate: DF.Float
|
||||
currency: DF.Link
|
||||
default_warehouse: DF.Link | None
|
||||
error_log: DF.Text | None
|
||||
fg_warehouse: DF.Link | None
|
||||
item_code: DF.Link
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
items: DF.Table[BOMCreatorItem]
|
||||
operation: DF.Link | None
|
||||
operation_time: DF.Float
|
||||
plc_conversion_rate: DF.Float
|
||||
price_list_currency: DF.Link | None
|
||||
project: DF.Link | None
|
||||
@@ -61,8 +65,15 @@ class BOMCreator(Document):
|
||||
remarks: DF.TextEditor | None
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
set_rate_based_on_warehouse: DF.Check
|
||||
skip_material_transfer: DF.Check
|
||||
source_warehouse: DF.Link | None
|
||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||
track_operations: DF.Check
|
||||
track_semi_finished_goods: DF.Check
|
||||
uom: DF.Link | None
|
||||
wip_warehouse: DF.Link | None
|
||||
workstation: DF.Link | None
|
||||
workstation_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -80,6 +91,18 @@ class BOMCreator(Document):
|
||||
if row.is_expandable and row.item_code == self.item_code:
|
||||
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
|
||||
|
||||
if not row.parent_row_no and row.fg_item and row.fg_item != self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: set Parent Row No for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Set Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
elif row.parent_row_no and row.fg_item == self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: Parent Row No cannot be set for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Remove Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
def set_status(self, save=False):
|
||||
self.status = {
|
||||
0: "Draft",
|
||||
@@ -224,8 +247,10 @@ class BOMCreator(Document):
|
||||
|
||||
self.db_set("status", "In Progress")
|
||||
production_item_wise_rm = OrderedDict({})
|
||||
|
||||
final_product = (self.item_code, self.name)
|
||||
production_item_wise_rm.setdefault(
|
||||
(self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
|
||||
final_product, frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
|
||||
)
|
||||
|
||||
for row in self.items:
|
||||
@@ -245,9 +270,15 @@ class BOMCreator(Document):
|
||||
|
||||
try:
|
||||
for d in reverse_tree:
|
||||
if self.track_operations and self.track_semi_finished_goods and final_product == d:
|
||||
continue
|
||||
|
||||
fg_item_data = production_item_wise_rm.get(d).fg_item_data
|
||||
self.create_bom(fg_item_data, production_item_wise_rm)
|
||||
|
||||
if self.track_operations and self.track_semi_finished_goods:
|
||||
self.make_bom_for_final_product(production_item_wise_rm)
|
||||
|
||||
frappe.msgprint(_("BOMs created successfully"))
|
||||
except Exception:
|
||||
traceback = frappe.get_traceback(with_context=True)
|
||||
@@ -260,6 +291,81 @@ class BOMCreator(Document):
|
||||
|
||||
frappe.msgprint(_("BOMs creation failed"))
|
||||
|
||||
def make_bom_for_final_product(self, production_item_wise_rm):
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.update(
|
||||
{
|
||||
"item": self.item_code,
|
||||
"bom_type": "Production",
|
||||
"quantity": self.qty,
|
||||
"allow_alternative_item": 1,
|
||||
"bom_creator": self.name,
|
||||
"bom_creator_item": self.name,
|
||||
"rm_cost_as_per": "Manual",
|
||||
"with_operations": 1,
|
||||
"track_semi_finished_goods": 1,
|
||||
}
|
||||
)
|
||||
|
||||
for field in BOM_FIELDS:
|
||||
if self.get(field):
|
||||
bom.set(field, self.get(field))
|
||||
|
||||
for item in self.items:
|
||||
if not item.is_expandable or not item.operation:
|
||||
continue
|
||||
|
||||
bom.append(
|
||||
"operations",
|
||||
{
|
||||
"operation": item.operation,
|
||||
"workstation": item.workstation,
|
||||
"source_warehouse": item.source_warehouse,
|
||||
"wip_warehouse": item.wip_warehouse,
|
||||
"fg_warehouse": item.fg_warehouse,
|
||||
"finished_good": item.item_code,
|
||||
"finished_good_qty": item.qty,
|
||||
"bom_no": production_item_wise_rm[(item.item_code, item.name)].bom_no,
|
||||
"workstation_type": item.workstation_type,
|
||||
"time_in_mins": item.operation_time,
|
||||
"is_subcontracted": item.is_subcontracted,
|
||||
"skip_material_transfer": item.skip_material_transfer,
|
||||
"backflush_from_wip_warehouse": item.backflush_from_wip_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
operation_row = bom.append(
|
||||
"operations",
|
||||
{
|
||||
"operation": self.operation,
|
||||
"time_in_mins": self.operation_time,
|
||||
"workstation": self.workstation,
|
||||
"workstation_type": self.workstation_type,
|
||||
"finished_good": self.item_code,
|
||||
"finished_good_qty": self.qty,
|
||||
"source_warehouse": self.source_warehouse,
|
||||
"wip_warehouse": self.wip_warehouse,
|
||||
"fg_warehouse": self.fg_warehouse,
|
||||
"skip_material_transfer": self.skip_material_transfer,
|
||||
"backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
final_product = (self.item_code, self.name)
|
||||
items = production_item_wise_rm.get(final_product).get("items")
|
||||
|
||||
bom.set_materials_based_on_operation_bom()
|
||||
|
||||
for item in items:
|
||||
item_args = {"operation_row_id": operation_row.idx}
|
||||
for field in BOM_ITEM_FIELDS:
|
||||
item_args[field] = item.get(field)
|
||||
|
||||
bom.append("items", item_args)
|
||||
|
||||
bom.save(ignore_permissions=True)
|
||||
bom.submit()
|
||||
|
||||
def create_bom(self, row, production_item_wise_rm):
|
||||
bom_creator_item = row.name if row.name != self.name else ""
|
||||
if frappe.db.exists(
|
||||
@@ -285,6 +391,24 @@ class BOMCreator(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if self.track_operations and not self.track_semi_finished_goods:
|
||||
if row.item_code == self.item_code:
|
||||
bom.with_operations = 1
|
||||
bom.transfer_material_against = "Work Order"
|
||||
for item in self.items:
|
||||
if not item.operation:
|
||||
continue
|
||||
|
||||
bom.append(
|
||||
"operations",
|
||||
{
|
||||
"operation": item.operation,
|
||||
"workstation_type": item.workstation_type,
|
||||
"workstation": item.workstation,
|
||||
"time_in_mins": item.operation_time,
|
||||
},
|
||||
)
|
||||
|
||||
for field in BOM_FIELDS:
|
||||
if self.get(field):
|
||||
bom.set(field, self.get(field))
|
||||
@@ -340,6 +464,16 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
"uom",
|
||||
"rate",
|
||||
"amount",
|
||||
"workstation_type",
|
||||
"operation",
|
||||
"operation_time",
|
||||
"is_subcontracted",
|
||||
"workstation",
|
||||
"source_warehouse",
|
||||
"wip_warehouse",
|
||||
"fg_warehouse",
|
||||
"skip_material_transfer",
|
||||
"backflush_from_wip_warehouse",
|
||||
]
|
||||
|
||||
query_filters = {
|
||||
@@ -353,6 +487,12 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||
|
||||
|
||||
def get_parent_row_no(doc, name):
|
||||
for row in doc.items:
|
||||
if row.name == name:
|
||||
return row.idx
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
@@ -363,6 +503,11 @@ def add_item(**kwargs):
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
@@ -371,6 +516,9 @@ def add_item(**kwargs):
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
doc.append("items", kwargs)
|
||||
doc.save()
|
||||
|
||||
@@ -390,6 +538,7 @@ def add_sub_assembly(**kwargs):
|
||||
|
||||
name = kwargs.fg_reference_id
|
||||
parent_row_no = ""
|
||||
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
item_row = doc.append(
|
||||
@@ -405,11 +554,37 @@ def add_sub_assembly(**kwargs):
|
||||
"do_not_explode": 1,
|
||||
"is_expandable": 1,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"operation": bom_item.operation,
|
||||
"workstation_type": bom_item.workstation_type,
|
||||
"operation_time": bom_item.operation_time,
|
||||
"is_subcontracted": bom_item.is_subcontracted,
|
||||
"workstation": bom_item.workstation,
|
||||
"source_warehouse": bom_item.source_warehouse,
|
||||
"wip_warehouse": bom_item.wip_warehouse,
|
||||
"fg_warehouse": bom_item.fg_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||
if parent_row_no:
|
||||
parent_row_no = parent_row_no[0]
|
||||
doc.items[parent_row_no - 1].update(
|
||||
{
|
||||
"operation": bom_item.operation,
|
||||
"workstation_type": bom_item.workstation_type,
|
||||
"operation_time": bom_item.operation_time,
|
||||
"is_subcontracted": bom_item.is_subcontracted,
|
||||
"workstation": bom_item.workstation,
|
||||
"source_warehouse": bom_item.source_warehouse,
|
||||
"wip_warehouse": bom_item.wip_warehouse,
|
||||
"fg_warehouse": bom_item.fg_warehouse,
|
||||
"skip_material_transfer": bom_item.skip_material_transfer,
|
||||
"backflush_from_wip_warehouse": bom_item.backflush_from_wip_warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
@@ -466,10 +641,16 @@ def delete_node(**kwargs):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_qty(doctype, docname, qty, parent):
|
||||
frappe.db.set_value(doctype, docname, "qty", qty)
|
||||
def edit_bom_creator(doctype, docname, data, parent):
|
||||
if isinstance(data, str):
|
||||
data = frappe.parse_json(data)
|
||||
|
||||
frappe.db.set_value(doctype, docname, data)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", parent)
|
||||
doc.set_rate_for_items()
|
||||
doc.save()
|
||||
|
||||
frappe.msgprint(_("Updated successfully"), alert=True)
|
||||
|
||||
return doc
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user